├── .flake8 ├── .github └── FUNDING.yml ├── .gitignore ├── 1.1 Black-Scholes numerical methods.ipynb ├── 1.2 SDE simulations and statistics.ipynb ├── 1.3 Fourier transform methods.ipynb ├── 1.4 SDE - Heston model.ipynb ├── 1.5 SDE - Lévy processes.ipynb ├── 2.1 Black-Scholes PDE and sparse matrices.ipynb ├── 2.2 Exotic options.ipynb ├── 2.3 American Options.ipynb ├── 3.1 Merton jump-diffusion, PIDE method.ipynb ├── 3.2 Variance Gamma model, PIDE method.ipynb ├── 3.3 Pricing with the NIG Process.ipynb ├── 4.1 Option pricing with transaction costs.ipynb ├── 4.2 Volatility smile and model calibration.ipynb ├── 5.1 Linear regression - Kalman filter.ipynb ├── 5.2 Kalman auto-correlation tracking - AR(1) process.ipynb ├── 5.3 Volatility tracking.ipynb ├── 6.1 Ornstein-Uhlenbeck process and applications.ipynb ├── 7.1 Classical MVO.ipynb ├── A.1 Solution of linear equations.ipynb ├── A.2 Optimize and speed up the code. (SOR algorithm, Cython and C).ipynb ├── A.3 Introduction to Lévy processes and PIDEs.pdf ├── CITATION.cff ├── Dockerfile ├── LICENSE ├── README.md ├── data ├── historical_data.csv ├── spy-options-exp-2020-07-10-weekly-show-all-stacked-07-05-2020.csv ├── spy-options-exp-2021-01-15-weekly-show-all-stacked-07-05-2020.csv └── stocks_data.csv ├── docker-compose.yml ├── environment.yml ├── latex ├── A.3 Introduction to Lévy processes and PIDEs.bbl └── A.3 Introduction to Lévy processes and PIDEs.tex ├── list_of_packages.txt ├── pyproject.toml ├── requirements.txt ├── setup.py └── src ├── C ├── BS_SOR_main.c ├── BS_sor ├── Makefile ├── PDE_solver.c ├── PDE_solver.h ├── SOR.c ├── SOR.h └── mainSOR.c └── FMNM ├── BS_pricer.py ├── CF.py ├── FFT.py ├── Heston_pricer.py ├── Kalman_filter.py ├── Merton_pricer.py ├── NIG_pricer.py ├── Parameters.py ├── Processes.py ├── Solvers.py ├── TC_pricer.py ├── VG_pricer.py ├── __init__.py ├── cost_utils.py ├── cython ├── __init__.py ├── heston.pyx └── solvers.pyx ├── portfolio_optimization.py └── probabilities.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E266 # too many leading '#' for block comment 4 | W605 # invalid escape sequence '\i' 5 | E203, # E203: whitespace before ':' 6 | W503 # W503: line break before binary operator 7 | max-line-length = 120 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cantaro86 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.aux 2 | #*.bbl 3 | *.blg 4 | *.log 5 | *.maf 6 | *.toc 7 | *.mtc* 8 | *.out 9 | *backup 10 | 11 | *.sty 12 | *.png 13 | #*.pdf 14 | *~ 15 | .DS_Store 16 | 17 | .ipynb_checkpoints 18 | temp 19 | build 20 | python-venv 21 | src/FMNM/__pycache__/ 22 | src/FMNM/cython/build/ 23 | src/FMNM/cython/__pycache__/ 24 | src/C/*.o 25 | src/FMNM/FMNM.egg-info 26 | src/FMNM.egg-info 27 | src/FMNM/cython/*.so 28 | src/FMNM/cython/*.c 29 | -------------------------------------------------------------------------------- /A.2 Optimize and speed up the code. (SOR algorithm, Cython and C).ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# How to optimize the code?\n", 8 | "\n", 9 | "In this notebook I want to show how to write efficient code and how cython and C code can help to improve the speed. \n", 10 | "I decided to consider as example the SOR algorithm. Here we can see how the algorithm presented in the Notebook **A.1 Solution of linear equations** can be modified for our specific needs (i.e. solving PDEs). \n", 11 | "\n", 12 | "Again, if you are curious about the SOR and want to know more, have a look at the wiki page [link](https://en.wikipedia.org/wiki/Successive_over-relaxation).\n", 13 | "\n", 14 | "## Contents\n", 15 | " - [Python implelentation](#sec1)\n", 16 | " - [Cython](#sec2)\n", 17 | " - [C code](#sec3)\n", 18 | " - [BS python vs C](#sec3.1) " 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "import os\n", 28 | "import subprocess\n", 29 | "import numpy as np\n", 30 | "import scipy as scp\n", 31 | "from scipy.linalg import norm\n", 32 | "from FMNM.Solvers import SOR, SOR2\n", 33 | "\n", 34 | "%load_ext cython\n", 35 | "import cython" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "N = 3000\n", 45 | "aa = 2\n", 46 | "bb = 10\n", 47 | "cc = 5\n", 48 | "A = np.diag(aa * np.ones(N - 1), -1) + np.diag(bb * np.ones(N), 0) + np.diag(cc * np.ones(N - 1), 1)\n", 49 | "x = 2 * np.ones(N)\n", 50 | "b = A @ x" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "Here we use a tridiagonal matrix A \n", 58 | "\n", 59 | "$$ \\left(\n", 60 | "\\begin{array}{ccccc}\n", 61 | "bb & cc & 0 & \\cdots & 0 \\\\\n", 62 | "aa & bb & cc & 0 & 0 \\\\\n", 63 | "0 & \\ddots & \\ddots & \\ddots & 0 \\\\\n", 64 | "\\vdots & 0 & aa & bb & cc \\\\\n", 65 | "0 & 0 & 0 & aa & bb \\\\\n", 66 | "\\end{array}\n", 67 | "\\right) $$\n", 68 | "\n", 69 | "with equal elements in the three diagonals: \n", 70 | "\n", 71 | "$$ aa = 2, \\quad bb = 10, \\quad cc = 5 $$\n", 72 | "\n", 73 | "This is the case of the Black-Scholes equation (in log-variables). \n", 74 | "The matrix A is quite big because we want to test the performances of the algorithms.\n", 75 | "\n", 76 | "The linear system is always the same: \n", 77 | "\n", 78 | "$$ A x = b$$\n", 79 | "\n", 80 | "For simplicity I chose $x = [2,...,2]$. " 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "\n", 88 | "## Python implementation\n", 89 | "\n", 90 | "I wrote two functions to implement the SOR algorithm with the aim of solving PDEs. \n", 91 | " - ```SOR``` uses matrix multiplications. The code is the same presented in the notebook **A1**: First it creates the matrices D,U,L (if A is sparse, it is converted into a numpy.array). Then it iterates the solutions until convergence. \n", 92 | " - ```SOR2``` iterates over all components of $x$ . It does not perform matrix multiplications but it considers each component of $x$ for the computations. \n", 93 | "The algorithm is the following: \n", 94 | "\n", 95 | "```python\n", 96 | " x0 = np.ones_like(b, dtype=np.float64) # initial guess\n", 97 | " x_new = np.ones_like(x0) # new solution\n", 98 | " \n", 99 | " for k in range(1,N_max+1): # iteration until convergence\n", 100 | " for i in range(N): # iteration over all the rows\n", 101 | " S = 0\n", 102 | " for j in range(N): # iteration over the columns\n", 103 | " if j != i:\n", 104 | " S += A[i,j]*x_new[j]\n", 105 | " x_new[i] = (1-w)*x_new[i] + (w/A[i,i]) * (b[i] - S) \n", 106 | " \n", 107 | " if norm(x_new - x0) < eps: # check convergence\n", 108 | " return x_new\n", 109 | " x0 = x_new.copy() # updates the solution \n", 110 | " if k==N_max:\n", 111 | " print(\"Fail to converge in {} iterations\".format(k))\n", 112 | "```\n", 113 | "This algorithm is taken from the SOR wiki [page](https://en.wikipedia.org/wiki/Successive_over-relaxation) and it is equivalent to the algorithm presented in the notebook **A1**.\n", 114 | "\n", 115 | "Let us see how fast they are: (well... how **slow** they are... be ready to wait about 6 minutes)" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 3, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "CPU times: user 979 ms, sys: 279 ms, total: 1.26 s\n", 128 | "Wall time: 869 ms\n" 129 | ] 130 | }, 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "array([2., 2., 2., ..., 2., 2., 2.])" 135 | ] 136 | }, 137 | "execution_count": 3, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "%%time\n", 144 | "SOR(A, b)" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 4, 150 | "metadata": {}, 151 | "outputs": [ 152 | { 153 | "name": "stdout", 154 | "output_type": "stream", 155 | "text": [ 156 | "CPU times: user 59.9 s, sys: 597 ms, total: 1min\n", 157 | "Wall time: 1min\n" 158 | ] 159 | }, 160 | { 161 | "data": { 162 | "text/plain": [ 163 | "array([2., 2., 2., ..., 2., 2., 2.])" 164 | ] 165 | }, 166 | "execution_count": 4, 167 | "metadata": {}, 168 | "output_type": "execute_result" 169 | } 170 | ], 171 | "source": [ 172 | "%%time\n", 173 | "SOR2(A, b)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "## TOO BAD!\n", 181 | "\n", 182 | "The second algorithm is very bad. There is an immediate improvement to do: \n", 183 | "We are working with a **tridiagonal matrix**. It means that all the elements not on the three diagonals are zero. The first piece of code to modify is OBVIOUSLY this:\n", 184 | "```python\n", 185 | "for j in range(N): # iteration over the columns\n", 186 | " if j != i:\n", 187 | " S += A[i,j]*x_new[j]\n", 188 | "``` \n", 189 | "There is no need to sum zero elements. \n", 190 | "Let us consider the new function:" 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": 5, 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "def SOR3(A, b, w=1, eps=1e-10, N_max=100):\n", 200 | " N = len(b)\n", 201 | " x0 = np.ones_like(b, dtype=np.float64) # initial guess\n", 202 | " x_new = np.ones_like(x0) # new solution\n", 203 | " for k in range(1, N_max + 1):\n", 204 | " for i in range(N):\n", 205 | " if i == 0: # new code start\n", 206 | " S = A[0, 1] * x_new[1]\n", 207 | " elif i == N - 1:\n", 208 | " S = A[N - 1, N - 2] * x_new[N - 2]\n", 209 | " else:\n", 210 | " S = A[i, i - 1] * x_new[i - 1] + A[i, i + 1] * x_new[i + 1]\n", 211 | " # new code end\n", 212 | " x_new[i] = (1 - w) * x_new[i] + (w / A[i, i]) * (b[i] - S)\n", 213 | " if norm(x_new - x0) < eps:\n", 214 | " return x_new\n", 215 | " x0 = x_new.copy()\n", 216 | " if k == N_max:\n", 217 | " print(\"Fail to converge in {} iterations\".format(k))" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 6, 223 | "metadata": {}, 224 | "outputs": [ 225 | { 226 | "name": "stdout", 227 | "output_type": "stream", 228 | "text": [ 229 | "83.5 ms ± 1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 230 | ] 231 | } 232 | ], 233 | "source": [ 234 | "%%timeit\n", 235 | "SOR3(A, b)" 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "### OK ... it was easy!\n", 243 | "\n", 244 | "But wait a second... if all the elements in the three diagonals are equal, do we really need a matrix? \n", 245 | "Of course, we can use sparse matrices to save space in memory. But do we really need any kind of matrix? \n", 246 | "The same algorithm can be written considering just the three values $aa,bb,cc$. \n", 247 | "\n", 248 | "**In the following algorithm, even if the gain in speed is not so much, we save a lot of space in memory!!** " 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 7, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "def SOR4(aa, bb, cc, b, w=1, eps=1e-10, N_max=100):\n", 258 | " N = len(b)\n", 259 | " x0 = np.ones_like(b, dtype=np.float64) # initial guess\n", 260 | " x_new = np.ones_like(x0) # new solution\n", 261 | " for k in range(1, N_max + 1):\n", 262 | " for i in range(N):\n", 263 | " if i == 0:\n", 264 | " S = cc * x_new[1]\n", 265 | " elif i == N - 1:\n", 266 | " S = aa * x_new[N - 2]\n", 267 | " else:\n", 268 | " S = aa * x_new[i - 1] + cc * x_new[i + 1]\n", 269 | " x_new[i] = (1 - w) * x_new[i] + (w / bb) * (b[i] - S)\n", 270 | " if norm(x_new - x0) < eps:\n", 271 | " return x_new\n", 272 | " x0 = x_new.copy()\n", 273 | " if k == N_max:\n", 274 | " print(\"Fail to converge in {} iterations\".format(k))\n", 275 | " return x_new" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": 8, 281 | "metadata": {}, 282 | "outputs": [ 283 | { 284 | "name": "stdout", 285 | "output_type": "stream", 286 | "text": [ 287 | "59 ms ± 242 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 288 | ] 289 | } 290 | ], 291 | "source": [ 292 | "%%timeit\n", 293 | "SOR4(aa, bb, cc, b)" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "\n", 301 | "## Cython\n", 302 | "\n", 303 | "\n", 304 | "For those who are not familiar with Cython, I suggest to read this introduction [link](https://cython.readthedocs.io/en/latest/src/userguide/numpy_tutorial.html).\n", 305 | "\n", 306 | "Cython, basically, consists in adding types to the python variables. \n", 307 | "\n", 308 | "Let's see what happens to the speed when we add types to the previous pure python function (SOR4)" 309 | ] 310 | }, 311 | { 312 | "cell_type": "code", 313 | "execution_count": 9, 314 | "metadata": {}, 315 | "outputs": [], 316 | "source": [ 317 | "%%cython --compile-args=-mcpu=apple-m2 --compile-args=-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION --compile-args=-w --force\n", 318 | "import numpy as np\n", 319 | "from scipy.linalg import norm\n", 320 | "cimport numpy as np \n", 321 | "cimport cython\n", 322 | "\n", 323 | "@cython.boundscheck(False) # turn off bounds-checking for entire function\n", 324 | "@cython.wraparound(False) # turn off negative index wrapping for entire function\n", 325 | "def SOR_cy(np.float64_t aa, \n", 326 | " np.float64_t bb, np.float64_t cc, \n", 327 | " np.ndarray[np.float64_t , ndim=1] b, \n", 328 | " double w=1, double eps=1e-10, int N_max = 100):\n", 329 | " \n", 330 | " cdef unsigned int N = b.size\n", 331 | " cdef np.ndarray[np.float64_t , ndim=1] x0 = np.ones(N, dtype=np.float64) # initial guess\n", 332 | " cdef np.ndarray[np.float64_t , ndim=1] x_new = np.ones(N, dtype=np.float64) # new solution\n", 333 | " cdef unsigned int i, k\n", 334 | " cdef np.float64_t S\n", 335 | " \n", 336 | " for k in range(1,N_max+1):\n", 337 | " for i in range(N):\n", 338 | " if (i==0):\n", 339 | " S = cc * x_new[1]\n", 340 | " elif (i==N-1):\n", 341 | " S = aa * x_new[N-2]\n", 342 | " else:\n", 343 | " S = aa * x_new[i-1] + cc * x_new[i+1]\n", 344 | " x_new[i] = (1-w)*x_new[i] + (w/bb) * (b[i] - S) \n", 345 | " if norm(x_new - x0) < eps:\n", 346 | " return x_new\n", 347 | " x0 = x_new.copy()\n", 348 | " if k==N_max:\n", 349 | " print(\"Fail to converge in {} iterations\".format(k))\n", 350 | " return x_new" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 10, 356 | "metadata": {}, 357 | "outputs": [ 358 | { 359 | "name": "stdout", 360 | "output_type": "stream", 361 | "text": [ 362 | "1.06 ms ± 1.09 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" 363 | ] 364 | } 365 | ], 366 | "source": [ 367 | "%%timeit\n", 368 | "SOR_cy(aa, bb, cc, b)" 369 | ] 370 | }, 371 | { 372 | "cell_type": "markdown", 373 | "metadata": {}, 374 | "source": [ 375 | "### About 100 times faster!!!\n", 376 | "\n", 377 | "That's good.\n", 378 | "\n", 379 | "So... those who are not familiar with Cython maybe are confused about the new type `np.float64_t`. We wrote: \n", 380 | "```python\n", 381 | "import numpy as np\n", 382 | "cimport numpy as np \n", 383 | "``` \n", 384 | "The first line imports numpy module in the python space. \n", 385 | "It only gives access to Numpy’s pure-Python API and it occurs at runtime.\n", 386 | "\n", 387 | "The second line gives access to the Numpy’s C API defined in the `__init__.pxd` file ([link to the file](https://github.com/cython/cython/blob/master/Cython/Includes/numpy/__init__.pxd)) during compile time. \n", 388 | "\n", 389 | "Even if they are both named `np`, they are automatically recognized.\n", 390 | "In `__init__.pdx` it is defined:\n", 391 | "```\n", 392 | "ctypedef double npy_float64\n", 393 | "ctypedef npy_float64 float64_t\n", 394 | "``` \n", 395 | "The `np.float64_t` represents the type `double` in C." 396 | ] 397 | }, 398 | { 399 | "cell_type": "markdown", 400 | "metadata": {}, 401 | "source": [ 402 | "### Memoryviews\n", 403 | "\n", 404 | "Let us re-write the previous code using the faster [memoryviews](https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html). \n", 405 | "I suggest to the reader to have a fast look at the memoryviews manual in the link. There are no difficult concepts and the notation is not so different from the notation used in the previous function. \n", 406 | "\n", 407 | "Memoryviews is another tool to help speed up the algorithm.\n", 408 | "\n", 409 | "I have to admit that when I was writing the new code I realized that using the function `norm` is not the optimal way. (I got an error because `norm` only accepts ndarrays... so, thanks memoryviews :) ). \n", 410 | "Well, the `norm` function computes a square root, which still requires some computations. \n", 411 | "We can define our own function `distance2` (which is the square of the distance) that is compared with the square of the tolerance parameter `eps * eps`. This is another improvement of the algorithm." 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": 11, 417 | "metadata": {}, 418 | "outputs": [], 419 | "source": [ 420 | "%%cython --compile-args=-O2 --compile-args=-mcpu=apple-m2 --compile-args=-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION --compile-args=-w --force\n", 421 | "import numpy as np\n", 422 | "cimport numpy as np\n", 423 | "cimport cython\n", 424 | "\n", 425 | "cdef double distance2(double[:] a, double[:] b, unsigned int N):\n", 426 | " cdef double dist = 0\n", 427 | " cdef unsigned int i \n", 428 | " for i in range(N):\n", 429 | " dist += (a[i] - b[i]) * (a[i] - b[i])\n", 430 | " return dist\n", 431 | "\n", 432 | "@cython.boundscheck(False)\n", 433 | "@cython.wraparound(False)\n", 434 | "def SOR_cy2(double aa, \n", 435 | " double bb, double cc, \n", 436 | " double[:] b, \n", 437 | " double w=1, double eps=1e-10, int N_max = 200):\n", 438 | " \n", 439 | " cdef unsigned int N = b.size \n", 440 | " cdef double[:] x0 = np.ones(N, dtype=np.float64) # initial guess\n", 441 | " cdef double[:] x_new = np.ones(N, dtype=np.float64) # new solution\n", 442 | " cdef unsigned int i, k\n", 443 | " cdef double S\n", 444 | " \n", 445 | " for k in range(1,N_max+1):\n", 446 | " for i in range(N):\n", 447 | " if (i==0):\n", 448 | " S = cc * x_new[1]\n", 449 | " elif (i==N-1):\n", 450 | " S = aa * x_new[N-2]\n", 451 | " else:\n", 452 | " S = aa * x_new[i-1] + cc * x_new[i+1]\n", 453 | " x_new[i] = (1-w)*x_new[i] + (w/bb) * (b[i] - S) \n", 454 | " if distance2(x_new, x0, N) < eps*eps:\n", 455 | " return np.asarray(x_new)\n", 456 | " x0[:] = x_new\n", 457 | " if k==N_max:\n", 458 | " print(\"Fail to converge in {} iterations\".format(k))\n", 459 | " return np.asarray(x_new)" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 12, 465 | "metadata": {}, 466 | "outputs": [ 467 | { 468 | "name": "stdout", 469 | "output_type": "stream", 470 | "text": [ 471 | "1.06 ms ± 5.02 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" 472 | ] 473 | } 474 | ], 475 | "source": [ 476 | "%%timeit\n", 477 | "SOR_cy2(aa, bb, cc, b)" 478 | ] 479 | }, 480 | { 481 | "cell_type": "markdown", 482 | "metadata": {}, 483 | "source": [ 484 | "### Good job!! Another improvement!" 485 | ] 486 | }, 487 | { 488 | "cell_type": "markdown", 489 | "metadata": {}, 490 | "source": [ 491 | "\n", 492 | "## C code\n", 493 | "\n", 494 | "The last improvement is to write the function in C code and call it from python. \n", 495 | "Inside the folder `src/C` you can find the header file `SOR.h` and the implementation file `SOR.c` (you will find also the `mainSOR.c` if you want to test the SOR algorithm directly in C). \n", 496 | "I will call the function `SOR_abc` declared in the header `SOR.h`. \n", 497 | "First it is declared as extern, and then it is called inside `SOR_c` with a cast to ``." 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": 13, 503 | "metadata": {}, 504 | "outputs": [], 505 | "source": [ 506 | "%%cython -I src/C --compile-args=-O2 --compile-args=-mcpu=apple-m2 --compile-args=-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION --compile-args=-w --force\n", 507 | "#\n", 508 | "# The %%cython directive must be the first keyword in the cell\n", 509 | "\n", 510 | "cdef extern from \"SOR.c\":\n", 511 | " pass\n", 512 | "cdef extern from \"SOR.h\":\n", 513 | " double* SOR_abc(double, double, double, double *, int, double, double, int)\n", 514 | "\n", 515 | "import numpy as np\n", 516 | "cimport cython\n", 517 | "\n", 518 | "@cython.boundscheck(False)\n", 519 | "@cython.wraparound(False)\n", 520 | "def SOR_c(double aa, double bb, double cc, B, double w=1, double eps=1e-10, int N_max = 200): \n", 521 | "\n", 522 | " if not B.flags['C_CONTIGUOUS']:\n", 523 | " B = np.ascontiguousarray(B) # Makes a contiguous copy of the numpy array\n", 524 | " \n", 525 | " cdef double[::1] arr_memview = B \n", 526 | " cdef double[::1] x = SOR_abc(aa, bb, cc, \n", 527 | " &arr_memview[0], arr_memview.shape[0], \n", 528 | " w, eps, N_max)\n", 529 | " return np.asarray(x)" 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": 14, 535 | "metadata": {}, 536 | "outputs": [ 537 | { 538 | "name": "stdout", 539 | "output_type": "stream", 540 | "text": [ 541 | "738 µs ± 3.53 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" 542 | ] 543 | } 544 | ], 545 | "source": [ 546 | "%%timeit\n", 547 | "SOR_c(aa, bb, cc, b)" 548 | ] 549 | }, 550 | { 551 | "cell_type": "markdown", 552 | "metadata": {}, 553 | "source": [ 554 | "## Well... it looks like that using Cython with memoryviews has the same performances as wrapping a C function.\n", 555 | "\n", 556 | "For this reason, I used the cython version as solver in the class `BS_pricer`. \n", 557 | "We already compared some performances in the notebook **1.2 - BS PDE**, and we saw that the SOR algorithm is slow compared to the LU or Thomas algorithms. \n", 558 | "Just for curiosity, let us compare the speed of the python PDE_price method implemented with cython SOR algorithm, and a pricer with same SOR algorithm fully implemented in C." 559 | ] 560 | }, 561 | { 562 | "cell_type": "code", 563 | "execution_count": 15, 564 | "metadata": {}, 565 | "outputs": [], 566 | "source": [ 567 | "from FMNM.Parameters import Option_param\n", 568 | "from FMNM.Processes import Diffusion_process\n", 569 | "from FMNM.BS_pricer import BS_pricer\n", 570 | "\n", 571 | "opt_param = Option_param(S0=100, K=100, T=1, exercise=\"European\", payoff=\"call\")\n", 572 | "diff_param = Diffusion_process(r=0.1, sig=0.2)\n", 573 | "BS = BS_pricer(opt_param, diff_param)" 574 | ] 575 | }, 576 | { 577 | "cell_type": "markdown", 578 | "metadata": {}, 579 | "source": [ 580 | "\n", 581 | "## BS python vs C" 582 | ] 583 | }, 584 | { 585 | "cell_type": "markdown", 586 | "metadata": {}, 587 | "source": [ 588 | "Run the command `make` to compile the C [code](./FMNM/C/PDE_solver.c):" 589 | ] 590 | }, 591 | { 592 | "cell_type": "code", 593 | "execution_count": 16, 594 | "metadata": {}, 595 | "outputs": [ 596 | { 597 | "name": "stdout", 598 | "output_type": "stream", 599 | "text": [ 600 | "clang -Wall -Werror -O2 -c -o BS_SOR_main.o BS_SOR_main.c\n", 601 | "clang -Wall -Werror -O2 -c -o SOR.o SOR.c\n", 602 | "clang -Wall -Werror -O2 -c -o PDE_solver.o PDE_solver.c\n", 603 | "clang -Wall -Werror -O2 -o BS_sor BS_SOR_main.o SOR.o PDE_solver.o -lm \n", 604 | " \n", 605 | "Compilation completed!\n", 606 | " \n" 607 | ] 608 | }, 609 | { 610 | "data": { 611 | "text/plain": [ 612 | "0" 613 | ] 614 | }, 615 | "execution_count": 16, 616 | "metadata": {}, 617 | "output_type": "execute_result" 618 | } 619 | ], 620 | "source": [ 621 | "os.system(\"cd ./src/C/ && make\")" 622 | ] 623 | }, 624 | { 625 | "cell_type": "markdown", 626 | "metadata": {}, 627 | "source": [ 628 | "Python program with Cython SOR method:" 629 | ] 630 | }, 631 | { 632 | "cell_type": "code", 633 | "execution_count": 17, 634 | "metadata": {}, 635 | "outputs": [ 636 | { 637 | "name": "stdout", 638 | "output_type": "stream", 639 | "text": [ 640 | "Price: 13.269170 Time: 7.370949\n" 641 | ] 642 | } 643 | ], 644 | "source": [ 645 | "print(\"Price: {0:.6f} Time: {1:.6f}\".format(*BS.PDE_price((3000, 2000), Time=True, solver=\"SOR\")))" 646 | ] 647 | }, 648 | { 649 | "cell_type": "markdown", 650 | "metadata": {}, 651 | "source": [ 652 | "Pure C program:" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": 18, 658 | "metadata": {}, 659 | "outputs": [ 660 | { 661 | "name": "stdout", 662 | "output_type": "stream", 663 | "text": [ 664 | "The price is: 13.269139 \n", 665 | " \n", 666 | "CPU times: user 821 µs, sys: 5.9 ms, total: 6.72 ms\n", 667 | "Wall time: 5.56 s\n" 668 | ] 669 | } 670 | ], 671 | "source": [ 672 | "%%time\n", 673 | "result = subprocess.run(\"./src/C/BS_sor\", stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n", 674 | "print(result.stdout.decode(\"utf-8\"))" 675 | ] 676 | } 677 | ], 678 | "metadata": { 679 | "kernelspec": { 680 | "display_name": "Python 3", 681 | "language": "python", 682 | "name": "python3" 683 | }, 684 | "language_info": { 685 | "codemirror_mode": { 686 | "name": "ipython", 687 | "version": 3 688 | }, 689 | "file_extension": ".py", 690 | "mimetype": "text/x-python", 691 | "name": "python", 692 | "nbconvert_exporter": "python", 693 | "pygments_lexer": "ipython3", 694 | "version": "3.11.4" 695 | } 696 | }, 697 | "nbformat": 4, 698 | "nbformat_minor": 2 699 | } 700 | -------------------------------------------------------------------------------- /A.3 Introduction to Lévy processes and PIDEs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantaro86/Financial-Models-Numerical-Methods/585ed19462665bd822c88b5b6e34fca8ad441370/A.3 Introduction to Lévy processes and PIDEs.pdf -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # https://bit.ly/cffinit 3 | 4 | cff-version: 1.2.0 5 | title: Financial Models Numerical Methods 6 | message: '"If you use software from this repo, please cite it as below."' 7 | type: software 8 | authors: 9 | - given-names: >- 10 | Nicola Cantarutti 11 | repository-code: 'https://github.com/cantaro86/Financial-Models-Numerical-Methods' 12 | date-released: 2019-06-09 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from this image 2 | FROM python:3.11.4-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /workspace 6 | # COPY . /workspace 7 | 8 | # Install Git, gcc, g++, cmake 9 | RUN apt-get update && \ 10 | apt-get install -y \ 11 | git \ 12 | gcc \ 13 | g++ \ 14 | cmake \ 15 | build-essential \ 16 | && apt-get clean \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | 20 | # Clone the GitHub repository 21 | RUN git clone https://github.com/cantaro86/Financial-Models-Numerical-Methods.git . 22 | 23 | # Install requirments 24 | RUN pip install --upgrade pip 25 | RUN pip install --no-cache-dir --requirement requirements.txt 26 | 27 | # Old style jupyter notebooks 28 | RUN pip install nbclassic 29 | 30 | # Install local package 31 | RUN pip install . 32 | 33 | # Expose the Jupyter Notebook port 34 | EXPOSE 8888 35 | 36 | # Start Jupyter Notebook server 37 | CMD ["jupyter", "nbclassic", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root", "--NotebookApp.token=''"] 38 | 39 | 40 | ################################################################# 41 | 42 | # 1. BUILD 43 | # docker build -t fmnm . 44 | 45 | # 2. CREATE CONTAINER 46 | # docker run --rm -d -p 8888:8888 --name Numeric_Finance fmnm 47 | 48 | # 3. OPEN IN BROWSER 49 | # http://localhost:8888/lab 50 | # or 51 | # http://localhost:8888/lab 52 | 53 | # OR 54 | 55 | # 1. docker-compose up --build -d --remove-orphans 56 | # 2. docker-compose down --rmi all --volumes --remove-orphans 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Financial-Models-Numerical-Methods 2 | ================================== 3 | 4 | 5 | This is a collection of [Jupyter notebooks](https://jupyter.org/) based on different topics in the area of quantitative finance. 6 | 7 | 8 | ### Is this a tutorial? 9 | 10 | Almost! :) 11 | 12 | This is just a collection of topics and algorithms that in my opinion are interesting. 13 | 14 | It contains several topics that are not so popular nowadays, but that can be very powerful. 15 | Usually, topics such as PDE methods, Lévy processes, Fourier methods or Kalman filter are not very popular among practitioners, who prefers to work with more standard tools. 16 | The aim of these notebooks is to present these interesting topics, by showing their practical application through an interactive python implementation. 17 | 18 | 19 | ### Who are these notebooks for? 20 | 21 | Not for absolute beginners. 22 | 23 | These topics require a basic knowledge in stochastic calculus, financial mathematics and statistics. A basic knowledge of python programming is also necessary. 24 | 25 | In these notebooks I will not explain what is a call option, or what is a stochastic process, or a partial differential equation. 26 | However, every time I will introduce a concept, I will also add a link to the corresponding wiki page or to a reference manual. 27 | In this way, the reader will be able to immediately understand what I am talking about. 28 | 29 | These notes are for students in science, economics or finance who have followed at least one undergraduate course in financial mathematics and statistics. 30 | Self-taught students or practicioners should have read at least an introductiory book on financial mathematics. 31 | 32 | 33 | ### Why is it worth to read these notes? 34 | 35 | First of all, this is not a book! 36 | Every notebook is (almost) independent from the others. Therefore you can select only the notebook you are interested in! 37 | 38 | ```diff 39 | - Every notebook contains python code ready to use! 40 | ``` 41 | 42 | It is not easy to find on internet examples of financial models implemented in python which are ready to use and well documented. 43 | I think that beginners in quantitative finance will find these notebooks very useful! 44 | 45 | Moreover, Jupyter notebooks are interactive i.e. you can run the code inside the notebook. 46 | This is probably the best way to study! 47 | 48 | If you open a notebook with Github or [NBviewer](https://nbviewer.ipython.org), sometimes mathematical formulas are not displayed correctly. 49 | For this reason, I suggest you to clone/download the repository. 50 | 51 | 52 | ### Is this series of notebooks complete? 53 | 54 | **No!** 55 | I will upload more notebooks from time to time. 56 | 57 | At the moment, I'm interested in the areas of stochastic processes, Kalman Filter, statistics and much more. I will add more interesting notebooks on these topics in the future. 58 | 59 | If you have any kind of questions, or if you find some errors, or you have suggestions for improvements, feel free to contact me. 60 | 61 | 62 | 63 | ### Contents 64 | 65 | 1.1) **Black-Scholes numerical methods** 66 | *(lognormal distribution, change of measure, Monte Carlo, Binomial method)*. 67 | 68 | 1.2) **SDE simulation and statistics** 69 | *(paths generation, Confidence intervals, Hypothesys testing, Geometric Brownian motion, Cox-Ingersoll-Ross process, Euler Maruyama method, parameters estimation)* 70 | 71 | 1.3) **Fourier inversion methods** 72 | *(inversion formula, numerical inversion, option pricing, FFT, Lewis formula)* 73 | 74 | 1.4) **SDE, Heston model** 75 | *(correlated Brownian motions, Heston paths, Heston distribution, characteristic function, option pricing)* 76 | 77 | 1.5) **SDE, Lévy processes** 78 | *(Merton, Variance Gamma, NIG, path generation, parameter estimation)* 79 | 80 | 2.1) **The Black-Scholes PDE** 81 | *(PDE discretization, Implicit method, sparse matrix tutorial)* 82 | 83 | 2.2) **Exotic options** 84 | *(Binary options, Barrier options, Asian options)* 85 | 86 | 2.3) **American options** 87 | *(PDE, Early exercise, Binomial method, Longstaff-Schwartz, Perpetual put)* 88 | 89 | 3.1) **Merton Jump-Diffusion PIDE** 90 | *(Implicit-Explicit discretization, discrete convolution, model limitations, Monte Carlo, Fourier inversion, semi-closed formula )* 91 | 92 | 3.2) **Variance Gamma PIDE** 93 | *(approximated jump-diffusion PIDE, Monte Carlo, Fourier inversion, Comparison with Black-Scholes)* 94 | 95 | 3.3) **Normal Inverse Gaussian PIDE** 96 | *(approximated jump-diffusion PIDE, Monte Carlo, Fourier inversion, properties of the Lévy measure)* 97 | 98 | 4.1) **Pricing with transaction costs** 99 | *(Davis-Panas-Zariphopoulou model, singular control problem, HJB variational inequality, indifference pricing, binomial tree, performances)* 100 | 101 | 4.2) **Volatility smile and model calibration** 102 | *(Volatility smile, root finder methods, calibration methods)* 103 | 104 | 5.1) **Linear regression and Kalman filter** 105 | *(market data cleaning, Linear regression methods, Kalman filter design, choice of parameters)* 106 | 107 | 5.2) **Kalman auto-correlation tracking - AR(1) process** 108 | *(Autoregressive process, estimation methods, Kalman filter, Kalman smoother, variable autocorrelation tracking)* 109 | 110 | 5.3) **Volatility tracking** 111 | *(Heston simulation, hypothesis testing, distribution fitting, estimation methods, GARCH(1,1), Kalman filter, Kalman smoother)* 112 | 113 | 6.1) **Ornstein-Uhlenbeck process and applications** 114 | *(parameters estimation, hitting time, Vasicek PDE, Kalman filter, trading strategy)* 115 | 116 | 7.1) **Classical MVO** 117 | *(mean variance optimization, quadratic programming, only long and long-short, closed formula)* 118 | 119 | A.1) **Appendix: Linear equations** 120 | *(LU, Jacobi, Gauss-Seidel, SOR, Thomas)* 121 | 122 | A.2) **Appendix: Code optimization** 123 | *(cython, C code)* 124 | 125 | A.3) **Appendix: Review of Lévy processes theory** 126 | *(basic and important definitions, derivation of the pricing PIDE)* 127 | 128 | 129 | 130 | ## How to run the notebooks 131 | 132 | 133 | **Virtual environment:** 134 | 135 | Here I explain how to create a virtual environment with [Anaconda](https://www.anaconda.com/distribution/) and with the python module [venv](https://docs.python.org/3.7/tutorial/venv.html). 136 | 137 | - Option 1: 138 | 139 | You can recreate my tested conda virtual environment with: 140 | 141 | ```bash 142 | conda env create -f environment.yml 143 | pip install -e . 144 | ``` 145 | 146 | The first line recreates the virtual environment and installs all the packages. 147 | With the second line we just install the local package `FMNM`. 148 | 149 | - Option 2: 150 | 151 | If you want to create a new environment with the latest python version, you can do: 152 | 153 | ```bash 154 | conda create -n FMNM python 155 | conda activate FMNM 156 | PACKAGES=$(tr '\n' ' ' < list_of_packages.txt | sed "s/arch/arch-py/g") 157 | conda install ${PACKAGES[@]} 158 | pip install -e . 159 | ``` 160 | 161 | where in the third line we replace the package name `arch` with the `arch-py`, which is the name used by conda. 162 | 163 | - Option 3: 164 | 165 | If you prefer to create a `venv` that uses python 3.11.4, you can do it as follows: 166 | 167 | ```bash 168 | python3.11.4 -m venv --prompt FMNM python-venv 169 | source python-venv/bin/activate 170 | python3 -m pip install --upgrade pip 171 | pip install --requirement requirements.txt 172 | pip install -e . 173 | ``` 174 | 175 | - Option 4: 176 | 177 | If you prefer to use the python version already installed in your system, you just need to run 178 | 179 | ```bash 180 | pip install --requirement list_of_packages.txt 181 | pip install -e . 182 | ``` 183 | 184 | and then enter in the shell `jupyter-notebook` or `jupyter-lab`: 185 | 186 | 187 | However, if you are using old versions, there could be compatibility problems. 188 | 189 | **Docker:** 190 | 191 | Here we run the notebooks with jupyterlab: 192 | 193 | - Option 1: 194 | 195 | You can use docker-compose to build a container: 196 | 197 | ```bash 198 | docker-compose up --build -d 199 | ``` 200 | 201 | And then stop the container with 202 | 203 | ```bash 204 | docker-compose down 205 | ``` 206 | 207 | And open the browser at `http://localhost:8888/lab` 208 | 209 | - Option 2: 210 | 211 | Alternatively, you can 212 | 213 | ```bash 214 | docker build -t fmnm . 215 | docker run --rm -d -p 8888:8888 --name Numeric_Finance fmnm 216 | ``` 217 | 218 | ### Enjoy! -------------------------------------------------------------------------------- /data/spy-options-exp-2020-07-10-weekly-show-all-stacked-07-05-2020.csv: -------------------------------------------------------------------------------- 1 | Strike,Last,% From Last,Bid,Midpoint,Ask,Change,%Chg,IV,Volume,Open Int,Type,Time 2 | 155,154.98,-50.36%,157.68,157.9,158.12,154.98,unch,235.16%,2,1,Call,06/22/20 3 | 160,0,-48.76%,152.68,152.9,153.12,0,unch,225.46%,0,0,Call,N/A 4 | 165,0,-47.15%,147.68,147.9,148.12,0,unch,216.04%,0,0,Call,N/A 5 | 170,0,-45.55%,142.68,142.9,143.12,0,unch,206.90%,0,0,Call,N/A 6 | 175,0,-43.95%,137.68,137.94,138.19,0,unch,199.79%,0,0,Call,N/A 7 | 180,0,-42.35%,132.68,132.9,133.12,0,unch,189.37%,0,0,Call,N/A 8 | 185,0,-40.75%,127.68,127.9,128.12,0,unch,180.95%,0,0,Call,N/A 9 | 190,0,-39.15%,122.68,122.91,123.14,0,unch,173.20%,0,0,Call,N/A 10 | 195,0,-37.55%,117.68,117.91,118.13,0,unch,164.95%,0,0,Call,N/A 11 | 200,102.05,-35.94%,112.68,112.9,113.12,-0.34,-0.33%,156.89%,10,30,Call,06/29/20 12 | 205,114.33,-34.34%,107.68,107.91,108.13,114.33,unch,149.44%,0,0,Call,N/A 13 | 210,0,-32.74%,102.68,102.91,103.13,0,unch,141.93%,0,0,Call,N/A 14 | 215,96.3,-31.14%,97.69,97.91,98.14,96.3,unch,134.96%,0,1,Call,N/A 15 | 220,0,-29.54%,92.69,92.91,93.13,0,unch,127.55%,0,0,Call,N/A 16 | 225,0,-27.94%,87.69,87.92,88.15,0,unch,120.81%,0,0,Call,N/A 17 | 230,0,-26.34%,82.7,82.93,83.15,0,unch,113.99%,0,0,Call,N/A 18 | 235,75,-24.73%,77.7,77.92,78.14,3,+4.17%,106.96%,1,4,Call,07/01/20 19 | 240,67.13,-23.13%,72.71,72.93,73.15,67.13,unch,100.50%,1,1,Call,06/15/20 20 | 245,59.24,-21.53%,67.71,67.93,68.15,59.24,unch,93.82%,2,2,Call,06/24/20 21 | 250,55.06,-19.93%,62.72,62.95,63.18,1.91,+3.59%,87.76%,2,6,Call,06/30/20 22 | 255,60.2,-18.33%,57.73,57.95,58.17,4.7,+8.47%,116.85%,2,5,Call,07/02/20 23 | 260,0,-16.73%,52.74,52.96,53.18,0,unch,74.97%,0,0,Call,N/A 24 | 265,0,-15.13%,47.75,47.97,48.19,0,unch,68.75%,0,0,Call,N/A 25 | 270,43.96,-13.53%,42.78,42.99,43.2,3.71,+9.22%,77.88%,1,22,Call,07/02/20 26 | 271,0,-13.21%,41.78,41.99,42.2,0,unch,61.46%,0,0,Call,N/A 27 | 272,41.72,-12.88%,40.77,40.99,41.21,41.72,unch,71.77%,1,0,Call,07/02/20 28 | 273,0,-12.56%,39.78,40,40.23,0,unch,59.18%,0,0,Call,N/A 29 | 274,38.86,-12.24%,38.78,39,39.22,2.12,+5.77%,55.03%,2,1,Call,07/02/20 30 | 275,35.75,-11.92%,37.79,38,38.22,4.75,+15.32%,56.61%,2,39,Call,07/01/20 31 | 276,26.17,-11.60%,36.8,37.02,37.24,-0.17,-0.65%,55.59%,1,26,Call,06/29/20 32 | 277,28.07,-11.28%,35.8,36.02,36.23,-5.56,-16.53%,54.21%,2,2,Call,06/25/20 33 | 278,29.71,-10.96%,34.81,35.03,35.25,1.04,+3.63%,53.18%,8,8,Call,06/25/20 34 | 279,32.84,-10.64%,33.81,34.03,34.24,4.07,+14.15%,51.81%,20,15,Call,07/01/20 35 | 280,34.42,-10.32%,32.82,33.04,33.25,2.57,+8.07%,68.02%,7,190,Call,07/02/20 36 | 281,21.8,-10.00%,31.82,32.04,32.26,-0.21,-0.95%,49.46%,19,13,Call,06/29/20 37 | 282,33.06,-9.68%,30.83,31.05,31.26,11.93,+56.46%,71.03%,1,11,Call,07/02/20 38 | 283,27.42,-9.36%,29.84,30.06,30.27,5.67,+26.07%,47.10%,1,7,Call,07/01/20 39 | 284,29.79,-9.04%,28.85,29.07,29.28,5.77,+24.02%,55.21%,5,11,Call,07/02/20 40 | 285,28.77,-8.72%,27.87,28.09,28.3,2.77,+10.65%,53.52%,21,76,Call,07/02/20 41 | 286,27.61,-8.40%,26.88,27.09,27.3,7.15,+34.95%,50.27%,1,45,Call,07/02/20 42 | 287,24.88,-8.08%,25.9,26.11,26.32,7.08,+39.78%,42.64%,25,67,Call,07/01/20 43 | 288,24.79,-7.76%,24.91,25.12,25.33,1.16,+4.91%,36.43%,1,63,Call,07/02/20 44 | 289,25.5,-7.44%,23.94,24.15,24.35,6.23,+32.33%,54.60%,10,124,Call,07/02/20 45 | 290,22.53,-7.12%,22.97,23.17,23.37,0.23,+1.03%,28.70%,130,1051,Call,07/02/20 46 | 291,18.72,-6.80%,21.99,22.2,22.4,3.06,+19.54%,38.36%,11,75,Call,06/30/20 47 | 292,23.05,-6.48%,21.02,21.23,21.43,5.35,+30.23%,54.29%,1,224,Call,07/02/20 48 | 293,20.76,-6.16%,20.06,20.27,20.47,2.7,+14.95%,41.54%,40,802,Call,07/02/20 49 | 294,20.39,-5.84%,19.1,19.3,19.5,2.74,+15.52%,45.59%,12,581,Call,07/02/20 50 | 295,18.3,-5.52%,18.15,18.34,18.53,1.88,+11.45%,33.98%,74,749,Call,07/02/20 51 | 296,17.46,-5.20%,17.21,17.4,17.59,1.79,+11.42%,34.16%,26,362,Call,07/02/20 52 | 297,16.13,-4.88%,16.26,16.45,16.64,1.24,+8.33%,29.34%,3,368,Call,07/02/20 53 | 298,14.96,-4.56%,15.34,15.53,15.71,0.53,+3.67%,26.05%,50,660,Call,07/02/20 54 | 299,13.77,-4.24%,14.41,14.59,14.78,0.9,+6.99%,22.40%,63,1027,Call,07/02/20 55 | 300,13.4,-3.92%,13.5,13.68,13.86,1.64,+13.95%,27.65%,405,1256,Call,07/02/20 56 | 301,13.9,-3.60%,12.65,12.78,12.9,2.95,+26.94%,37.50%,36,964,Call,07/02/20 57 | 302,11.3,-3.28%,11.75,11.88,12.01,0.43,+3.96%,23.74%,128,1257,Call,07/02/20 58 | 303,10.93,-2.96%,10.88,11,11.12,1.39,+14.57%,27.12%,349,2706,Call,07/02/20 59 | 304,9.93,-2.64%,10.02,10.13,10.23,1.17,+13.36%,25.41%,372,3194,Call,07/02/20 60 | 305,9.24,-2.32%,9.23,9.29,9.34,1.15,+14.22%,25.77%,519,3455,Call,07/02/20 61 | 306,8.33,-2.00%,8.4,8.46,8.51,1.12,+15.53%,24.51%,2830,2371,Call,07/02/20 62 | 307,7.58,-1.68%,7.59,7.65,7.71,1.2,+18.81%,24.15%,361,1624,Call,07/02/20 63 | 308,6.8,-1.35%,6.81,6.86,6.9,1,+17.24%,23.45%,643,1666,Call,07/02/20 64 | 309,6.08,-1.03%,6.06,6.1,6.13,0.96,+18.75%,22.95%,582,1776,Call,07/02/20 65 | 310,5.41,-0.71%,5.35,5.38,5.41,0.95,+21.30%,22.55%,9307,5297,Call,07/02/20 66 | 311,4.73,-0.39%,4.65,4.69,4.72,0.86,+22.22%,21.91%,1896,5228,Call,07/02/20 67 | 312,4.06,-0.07%,4.01,4.04,4.07,0.76,+23.03%,21.13%,8491,5568,Call,07/02/20 68 | 313,3.43,+0.25%,3.4,3.44,3.47,0.56,+19.51%,20.35%,3735,4812,Call,07/02/20 69 | 314,2.9,+0.57%,2.85,2.88,2.9,0.49,+20.33%,19.88%,9866,4792,Call,07/02/20 70 | 315,2.35,+0.89%,2.34,2.37,2.4,0.37,+18.69%,19.04%,20733,15901,Call,07/02/20 71 | 316,1.91,+1.21%,1.89,1.92,1.95,0.25,+15.06%,18.54%,7446,3717,Call,07/02/20 72 | 317,1.56,+1.53%,1.51,1.54,1.56,0.19,+13.87%,18.28%,6818,4313,Call,07/02/20 73 | 318,1.19,+1.85%,1.18,1.2,1.22,0.09,+8.18%,17.59%,5410,2882,Call,07/02/20 74 | 319,0.92,+2.17%,0.92,0.94,0.95,0,unch,17.23%,6387,2321,Call,07/02/20 75 | 320,0.71,+2.49%,0.71,0.72,0.72,-0.03,-4.05%,17.01%,42618,11969,Call,07/02/20 76 | 321,0.52,+2.81%,0.52,0.54,0.55,-0.06,-10.34%,16.61%,4372,1821,Call,07/02/20 77 | 322,0.38,+3.13%,0.39,0.4,0.41,-0.09,-19.15%,16.35%,3498,7159,Call,07/02/20 78 | 323,0.3,+3.45%,0.29,0.3,0.31,-0.09,-23.08%,16.50%,2905,1624,Call,07/02/20 79 | 324,0.23,+3.77%,0.21,0.22,0.23,-0.09,-28.13%,16.56%,2713,3120,Call,07/02/20 80 | 325,0.17,+4.09%,0.16,0.17,0.18,-0.09,-34.62%,16.52%,40153,24108,Call,07/02/20 81 | 326,0.13,+4.41%,0.12,0.13,0.13,-0.07,-35.00%,16.63%,2639,1842,Call,07/02/20 82 | 327,0.09,+4.73%,0.09,0.1,0.1,-0.09,-50.00%,16.47%,1376,2215,Call,07/02/20 83 | 328,0.07,+5.05%,0.07,0.08,0.08,-0.08,-53.33%,16.67%,1387,1777,Call,07/02/20 84 | 329,0.04,+5.37%,0.05,0.06,0.06,-0.1,-71.43%,16.14%,1241,1279,Call,07/02/20 85 | 330,0.04,+5.69%,0.04,0.05,0.05,-0.06,-60.00%,16.94%,5149,6136,Call,07/02/20 86 | 331,0.03,+6.01%,0.03,0.04,0.04,-0.07,-70.00%,17.07%,1948,3155,Call,07/02/20 87 | 332,0.02,+6.33%,0.02,0.03,0.03,-0.08,-80.00%,16.97%,779,194,Call,07/02/20 88 | 333,0.03,+6.65%,0.02,0.03,0.03,-0.03,-50.00%,18.60%,1016,2309,Call,07/02/20 89 | 334,0.02,+6.97%,0.01,0.02,0.02,-0.03,-60.00%,18.43%,737,134,Call,07/02/20 90 | 335,0.02,+7.29%,0.01,0.02,0.02,-0.03,-60.00%,19.15%,1676,3255,Call,07/02/20 91 | 336,0.02,+7.61%,0.01,0.02,0.02,-0.03,-60.00%,19.86%,499,916,Call,07/02/20 92 | 337,0.01,+7.93%,0,0.01,0.01,-0.04,-80.00%,19.08%,283,737,Call,07/02/20 93 | 338,0.01,+8.25%,0,0.01,0.01,-0.02,-66.67%,19.75%,46,599,Call,07/02/20 94 | 339,0.02,+8.57%,0,0.01,0.01,-0.02,-50.00%,21.98%,31,180,Call,07/02/20 95 | 340,0.01,+8.89%,0.01,0.02,0.02,-0.02,-66.67%,21.06%,244,2124,Call,07/02/20 96 | 341,0.01,+9.21%,0,0.01,0.01,-0.01,-50.00%,21.71%,217,712,Call,07/02/20 97 | 342,0.02,+9.53%,0,0.01,0.01,-0.01,-33.33%,24.05%,5,396,Call,07/02/20 98 | 343,0.01,+9.85%,0,0.01,0.01,-0.01,-50.00%,23.01%,1,618,Call,07/02/20 99 | 344,0.01,+10.18%,0,0.01,0.01,-0.01,-50.00%,23.65%,21,1003,Call,07/02/20 100 | 345,0.01,+10.50%,0,0.01,0.01,-0.01,-50.00%,24.28%,182,5391,Call,07/02/20 101 | 346,0.01,+10.82%,0,0.01,0.01,-0.01,-50.00%,24.91%,21,851,Call,07/02/20 102 | 347,0.01,+11.14%,0,0.01,0.01,-0.01,-50.00%,25.54%,19,645,Call,07/02/20 103 | 348,0.02,+11.46%,0,0.01,0.01,0,unch,0.00%,25,857,Call,07/01/20 104 | 349,0.01,+11.78%,0,0.01,0.01,-0.01,-50.00%,26.79%,3,776,Call,07/02/20 105 | 350,0.01,+12.10%,0,0.01,0.01,0,unch,27.41%,1,1679,Call,07/02/20 106 | 351,0.01,+12.42%,0,0.01,0.01,0,unch,0.00%,1,37,Call,07/01/20 107 | 352,0.02,+12.74%,0,0.01,0.01,0.01,+100.00%,0.00%,22,308,Call,07/01/20 108 | 353,0.01,+13.06%,0,0.01,0.01,-0.01,-50.00%,0.00%,23,114,Call,07/01/20 109 | 355,0.01,+13.70%,0,0.01,0.01,-0.02,-66.67%,0.00%,108,1311,Call,06/24/20 110 | 360,0.01,+15.30%,0,0.01,0.01,-0.01,-50.00%,0.00%,1,2252,Call,06/29/20 111 | 365,0.01,+16.90%,0,0.01,0.01,0,unch,0.00%,514,1722,Call,06/26/20 112 | 370,0.01,+18.50%,0,0.01,0.01,0,unch,0.00%,20,679,Call,06/24/20 113 | 375,0.02,+20.10%,0,0.01,0.01,-0.01,-33.33%,0.00%,7,485,Call,06/19/20 114 | 380,0.01,+21.71%,0,0.01,0.01,0,unch,0.00%,2292,3141,Call,06/26/20 115 | 385,0.03,+23.31%,0,0.01,0.01,0.01,+50.00%,0.00%,4,27,Call,06/16/20 116 | 390,0.01,+24.91%,0,0.01,0.01,0,unch,0.00%,5,143,Call,06/19/20 117 | 155,0.01,-50.36%,0,0.01,0.01,-0.01,-50.00%,0.00%,10,637,Put,07/01/20 118 | 160,0.01,-48.76%,0,0.01,0.01,-0.01,-50.00%,0.00%,3280,3809,Put,07/01/20 119 | 165,0.01,-47.15%,0,0.01,0.01,-0.01,-50.00%,0.00%,5340,5350,Put,07/01/20 120 | 170,0.01,-45.55%,0,0.01,0.01,-0.01,-50.00%,0.00%,65,772,Put,07/01/20 121 | 175,0.02,-43.95%,0,0.01,0.01,-0.03,-60.00%,0.00%,10,333,Put,06/29/20 122 | 180,0.01,-42.35%,0.01,0.02,0.02,-0.01,-50.00%,123.03%,100,194,Put,07/01/20 123 | 185,0.01,-40.75%,0.01,0.02,0.02,-0.01,-50.00%,113.39%,4409,760,Put,07/02/20 124 | 190,0.01,-39.15%,0.01,0.02,0.02,-0.01,-50.00%,107.95%,1,1171,Put,07/02/20 125 | 195,0.01,-37.55%,0.01,0.02,0.02,-0.01,-50.00%,102.65%,32,374,Put,07/02/20 126 | 200,0.02,-35.94%,0.01,0.02,0.02,0,unch,103.48%,103,3511,Put,07/02/20 127 | 205,0.02,-34.34%,0.01,0.02,0.02,0,unch,98.14%,1,1989,Put,07/02/20 128 | 210,0.01,-32.74%,0.01,0.02,0.02,-0.01,-50.00%,87.46%,1,2426,Put,07/02/20 129 | 215,0.02,-31.14%,0.01,0.02,0.02,-0.01,-33.33%,87.81%,100,1206,Put,07/02/20 130 | 220,0.02,-29.54%,0.01,0.02,0.02,-0.01,-33.33%,82.80%,349,994,Put,07/02/20 131 | 225,0.03,-27.94%,0.02,0.03,0.03,-0.01,-25.00%,81.00%,98,1284,Put,07/02/20 132 | 230,0.03,-26.34%,0.02,0.03,0.03,-0.01,-25.00%,76.01%,45,1611,Put,07/02/20 133 | 235,0.03,-24.73%,0.02,0.03,0.03,-0.02,-40.00%,71.12%,62,1344,Put,07/02/20 134 | 240,0.03,-23.13%,0.03,0.04,0.04,-0.02,-40.00%,66.30%,254,1531,Put,07/02/20 135 | 245,0.04,-21.53%,0.03,0.04,0.04,-0.03,-42.86%,63.47%,28,1940,Put,07/02/20 136 | 250,0.05,-19.93%,0.04,0.05,0.05,-0.03,-37.50%,60.17%,160,15442,Put,07/02/20 137 | 255,0.06,-18.33%,0.05,0.06,0.06,-0.03,-33.33%,56.55%,224,1765,Put,07/02/20 138 | 260,0.06,-16.73%,0.06,0.07,0.07,-0.04,-40.00%,51.70%,267,2815,Put,07/02/20 139 | 265,0.08,-15.13%,0.08,0.08,0.09,-0.04,-33.33%,48.65%,335,27523,Put,07/02/20 140 | 270,0.09,-13.53%,0.09,0.1,0.1,-0.06,-40.00%,44.46%,968,3044,Put,07/02/20 141 | 271,0.1,-13.21%,0.1,0.11,0.11,-0.08,-44.44%,44.11%,2,43,Put,07/02/20 142 | 272,0.1,-12.88%,0.1,0.11,0.11,-0.07,-41.18%,43.11%,22,1653,Put,07/02/20 143 | 273,0.11,-12.56%,0.1,0.11,0.11,-0.07,-38.89%,42.69%,28,1395,Put,07/02/20 144 | 274,0.1,-12.24%,0.11,0.11,0.12,-0.09,-47.37%,41.12%,135,1392,Put,07/02/20 145 | 275,0.12,-11.92%,0.11,0.11,0.12,-0.07,-36.84%,41.21%,2429,8750,Put,07/02/20 146 | 276,0.12,-11.60%,0.12,0.13,0.13,-0.08,-40.00%,40.19%,317,1995,Put,07/02/20 147 | 277,0.13,-11.28%,0.12,0.13,0.13,-0.1,-43.48%,39.66%,21,1301,Put,07/02/20 148 | 278,0.13,-10.96%,0.13,0.14,0.14,-0.08,-38.10%,38.64%,26,1196,Put,07/02/20 149 | 279,0.13,-10.64%,0.13,0.14,0.14,-0.1,-43.48%,37.62%,51,1003,Put,07/02/20 150 | 280,0.13,-10.32%,0.14,0.15,0.15,-0.11,-45.83%,36.60%,3094,19575,Put,07/02/20 151 | 281,0.15,-10.00%,0.15,0.16,0.16,-0.12,-44.44%,36.39%,85,772,Put,07/02/20 152 | 282,0.16,-9.68%,0.15,0.16,0.17,-0.12,-42.86%,35.73%,62,1726,Put,07/02/20 153 | 283,0.16,-9.36%,0.16,0.17,0.18,-0.14,-46.67%,34.69%,177,2486,Put,07/02/20 154 | 284,0.19,-9.04%,0.17,0.18,0.18,-0.13,-40.63%,34.64%,78,967,Put,07/02/20 155 | 285,0.19,-8.72%,0.18,0.19,0.2,-0.16,-45.71%,33.57%,3402,10898,Put,07/02/20 156 | 286,0.19,-8.40%,0.19,0.2,0.21,-0.17,-47.22%,32.50%,11058,12455,Put,07/02/20 157 | 287,0.22,-8.08%,0.21,0.22,0.22,-0.18,-45.00%,32.27%,452,1543,Put,07/02/20 158 | 288,0.23,-7.76%,0.23,0.24,0.24,-0.24,-51.06%,31.44%,549,1221,Put,07/02/20 159 | 289,0.26,-7.44%,0.24,0.25,0.26,-0.26,-50.00%,31.06%,6548,5594,Put,07/02/20 160 | 290,0.28,-7.12%,0.27,0.28,0.28,-0.29,-50.88%,30.38%,35643,31369,Put,07/02/20 161 | 291,0.3,-6.80%,0.29,0.3,0.31,-0.31,-50.82%,29.66%,1760,1856,Put,07/02/20 162 | 292,0.33,-6.48%,0.32,0.33,0.34,-0.33,-50.00%,29.09%,1769,1477,Put,07/02/20 163 | 293,0.36,-6.16%,0.35,0.36,0.37,-0.4,-52.63%,28.46%,986,1251,Put,07/02/20 164 | 294,0.4,-5.84%,0.39,0.4,0.41,-0.43,-51.81%,27.94%,4227,2062,Put,07/02/20 165 | 295,0.44,-5.52%,0.44,0.45,0.46,-0.48,-52.17%,27.34%,11132,10965,Put,07/02/20 166 | 296,0.5,-5.20%,0.49,0.5,0.51,-0.52,-50.98%,26.94%,1827,5776,Put,07/02/20 167 | 297,0.57,-4.88%,0.55,0.56,0.57,-0.55,-49.11%,26.57%,2413,5367,Put,07/02/20 168 | 298,0.61,-4.56%,0.61,0.63,0.64,-0.61,-50.00%,25.74%,1952,1605,Put,07/02/20 169 | 299,0.7,-4.24%,0.68,0.7,0.71,-0.69,-49.64%,25.41%,9437,2247,Put,07/02/20 170 | 300,0.78,-3.92%,0.77,0.79,0.8,-0.74,-48.68%,24.84%,18078,47109,Put,07/02/20 171 | 301,0.87,-3.60%,0.86,0.88,0.9,-0.88,-50.29%,24.28%,12127,10957,Put,07/02/20 172 | 302,1,-3.28%,0.96,0.98,1,-0.84,-45.65%,23.95%,4226,4486,Put,07/02/20 173 | 303,1.1,-2.96%,1.08,1.1,1.12,-0.93,-45.81%,23.24%,3867,6195,Put,07/02/20 174 | 304,1.23,-2.64%,1.22,1.23,1.25,-1.02,-45.33%,22.66%,10320,4532,Put,07/02/20 175 | 305,1.35,-2.32%,1.37,1.39,1.41,-1.12,-45.34%,21.88%,10779,6137,Put,07/02/20 176 | 306,1.59,-2.00%,1.54,1.56,1.58,-1.14,-41.76%,21.80%,2288,2697,Put,07/02/20 177 | 307,1.72,-1.68%,1.73,1.75,1.77,-1.25,-42.09%,20.81%,4480,2506,Put,07/02/20 178 | 308,1.94,-1.35%,1.94,1.96,1.98,-1.36,-41.21%,20.25%,10227,2888,Put,07/02/20 179 | 309,2.2,-1.03%,2.19,2.21,2.23,-1.52,-40.86%,19.75%,22270,3661,Put,07/02/20 180 | 310,2.46,-0.71%,2.45,2.48,2.5,-1.64,-40.00%,19.06%,40115,9546,Put,07/02/20 181 | 311,2.75,-0.39%,2.75,2.78,2.81,-1.7,-38.20%,18.33%,3894,4171,Put,07/02/20 182 | 312,3.11,-0.07%,3.1,3.14,3.17,-1.8,-36.66%,17.75%,6479,1612,Put,07/02/20 183 | 313,3.49,+0.25%,3.49,3.53,3.56,-1.92,-35.49%,17.02%,6651,813,Put,07/02/20 184 | 314,3.95,+0.57%,3.93,3.97,4.01,-2.14,-35.14%,16.45%,6984,514,Put,07/02/20 185 | 315,4.45,+0.89%,4.41,4.46,4.5,-2.28,-33.88%,15.78%,13969,2358,Put,07/02/20 186 | 316,5.01,+1.21%,4.96,5.01,5.05,-1.42,-22.08%,15.10%,1250,285,Put,07/02/20 187 | 317,6.36,+1.53%,5.57,5.62,5.67,-1.6,-20.10%,19.09%,1741,200,Put,07/02/20 188 | 318,7.1,+1.85%,6.24,6.29,6.34,-0.91,-11.36%,19.19%,822,317,Put,07/02/20 189 | 319,7.51,+2.17%,6.89,7.03,7.16,-1.94,-20.53%,16.62%,282,533,Put,07/02/20 190 | 320,7.75,+2.49%,7.65,7.82,7.98,-2.53,-24.61%,9.59%,2450,2740,Put,07/02/20 191 | 321,9.31,+2.81%,8.45,8.64,8.82,-1.68,-15.29%,17.76%,208,288,Put,07/02/20 192 | 322,10.7,+3.13%,9.31,9.5,9.7,-0.58,-5.14%,22.50%,181,160,Put,07/02/20 193 | 323,9.6,+3.45%,10.21,10.41,10.6,-7.68,-44.44%,0.00%,9,52,Put,07/02/20 194 | 324,9.43,+3.77%,11.13,11.33,11.53,-7.6,-44.63%,0.00%,5,37,Put,07/02/20 195 | 325,11.97,+4.09%,12.07,12.28,12.48,-2.8,-18.96%,4.35%,72,74,Put,07/02/20 196 | 326,11,+4.41%,13.02,13.23,13.44,-4.54,-29.21%,0.00%,1,37,Put,07/02/20 197 | 327,14.55,+4.73%,13.99,14.2,14.41,-9.54,-39.60%,10.14%,1,23,Put,07/02/20 198 | 328,17.94,+5.05%,14.96,15.18,15.39,-0.36,-1.97%,10.66%,10,43,Put,07/01/20 199 | 329,14.73,+5.37%,15.95,16.17,16.38,-4.87,-24.85%,0.00%,3,6,Put,07/02/20 200 | 330,15.98,+5.69%,16.93,17.15,17.36,-4.11,-20.46%,0.00%,29,90,Put,07/02/20 201 | 331,16.95,+6.01%,17.91,18.13,18.34,-11.09,-39.55%,0.00%,1,24,Put,07/02/20 202 | 332,17.23,+6.33%,18.92,19.14,19.35,0.7,+4.23%,0.00%,1,1,Put,07/02/20 203 | 333,24.88,+6.65%,19.91,20.13,20.34,-8.33,-25.08%,11.12%,8,23,Put,06/30/20 204 | 334,20.47,+6.97%,20.9,21.12,21.34,20.47,unch,22.62%,8,0,Put,07/02/20 205 | 335,26.77,+7.29%,21.9,22.12,22.33,-4.57,-14.58%,10.76%,7,7,Put,06/30/20 206 | 336,25.98,+7.61%,22.88,23.1,23.33,25.98,unch,10.17%,1,1,Put,06/16/20 207 | 337,0,+7.93%,23.89,24.11,24.33,0,unch,10.01%,0,0,Put,N/A 208 | 338,0,+8.25%,24.89,25.11,25.32,0,unch,9.28%,0,0,Put,N/A 209 | 339,33.4,+8.57%,25.89,26.11,26.33,33.4,unch,8.83%,0,2,Put,N/A 210 | 340,26.3,+8.89%,26.89,27.11,27.33,-3.1,-10.54%,22.55%,5,39,Put,07/02/20 211 | 341,23.05,+9.21%,27.89,28.11,28.33,23.05,unch,6.85%,0,50,Put,N/A 212 | 342,25.1,+9.53%,28.89,29.11,29.33,25.1,unch,5.46%,0,1,Put,N/A 213 | 343,0,+9.85%,29.89,30.11,30.33,0,unch,3.75%,0,0,Put,N/A 214 | 344,30.38,+10.18%,30.89,31.11,31.33,30.38,unch,22.99%,2,0,Put,07/02/20 215 | 345,0,+10.50%,31.89,32.11,32.33,0,unch,31.45%,0,0,Put,N/A 216 | 346,0,+10.82%,32.89,33.11,33.33,0,unch,31.56%,0,0,Put,N/A 217 | 347,0,+11.14%,33.89,34.11,34.33,0,unch,31.61%,0,0,Put,N/A 218 | 348,32.84,+11.46%,34.89,35.11,35.33,-10.52,-24.26%,0.19%,5,5,Put,07/02/20 219 | 349,0,+11.78%,35.89,36.11,36.33,0,unch,31.60%,0,0,Put,N/A 220 | 350,44.65,+12.10%,36.89,37.11,37.33,6.3,+16.43%,31.52%,15,15,Put,06/24/20 221 | 351,0,+12.42%,37.89,38.11,38.33,0,unch,31.39%,0,0,Put,N/A 222 | 352,0,+12.74%,38.89,39.11,39.33,0,unch,31.21%,0,0,Put,N/A 223 | 353,0,+13.06%,39.89,40.11,40.33,0,unch,30.98%,0,0,Put,N/A 224 | 355,0,+13.70%,41.89,42.11,42.33,0,unch,30.34%,0,0,Put,N/A 225 | 360,50.03,+15.30%,46.89,47.11,47.33,50.03,unch,27.54%,2,1,Put,06/22/20 226 | 365,0,+16.90%,51.89,52.11,52.33,0,unch,22.44%,0,0,Put,N/A 227 | 370,0,+18.50%,56.89,57.11,57.33,0,unch,13.87%,0,0,Put,N/A 228 | 375,62,+20.10%,61.9,62.11,62.33,62,unch,0.25%,2,0,Put,06/23/20 229 | 380,0,+21.71%,66.89,67.11,67.33,0,unch,0.00%,0,0,Put,N/A 230 | 385,75.19,+23.31%,71.9,72.12,72.33,2.19,+3.00%,0.00%,6,8,Put,06/18/20 231 | 390,77.95,+24.91%,76.88,77.1,77.33,-3.78,-4.62%,74.04%,3,5,Put,07/02/20 232 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | jupyterlab: 5 | image: fmnm 6 | build: . 7 | ports: 8 | - "8888:8888" 9 | volumes: 10 | - .:/workspace 11 | command: ["jupyter", "nbclassic", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root", "--NotebookApp.token=''"] 12 | container_name: Numeric_Finance 13 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: FMNM 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - anyio=3.7.1=pyhd8ed1ab_0 7 | - appnope=0.1.3=pyhd8ed1ab_0 8 | - arch-py=6.1.0=py311hb49d859_0 9 | - argon2-cffi=21.3.0=pyhd8ed1ab_0 10 | - argon2-cffi-bindings=21.2.0=py311he2be06e_3 11 | - asttokens=2.2.1=pyhd8ed1ab_0 12 | - async-lru=2.0.3=pyhd8ed1ab_0 13 | - attrs=23.1.0=pyh71513ae_1 14 | - babel=2.12.1=pyhd8ed1ab_1 15 | - backcall=0.2.0=pyh9f0ad1d_0 16 | - backports=1.0=pyhd8ed1ab_3 17 | - backports.functools_lru_cache=1.6.5=pyhd8ed1ab_0 18 | - beautifulsoup4=4.12.2=pyha770c72_0 19 | - black=23.7.0=py311h267d04e_1 20 | - black-jupyter=23.7.0=hd8ed1ab_1 21 | - bleach=6.0.0=pyhd8ed1ab_0 22 | - brotli=1.0.9=h1a8c8d9_9 23 | - brotli-bin=1.0.9=h1a8c8d9_9 24 | - brotli-python=1.0.9=py311ha397e9f_9 25 | - bzip2=1.0.8=h3422bc3_4 26 | - ca-certificates=2023.7.22=hf0a4a13_0 27 | - certifi=2023.7.22=pyhd8ed1ab_0 28 | - cffi=1.15.1=py311hae827db_3 29 | - charset-normalizer=3.2.0=pyhd8ed1ab_0 30 | - click=8.1.6=unix_pyh707e725_0 31 | - comm=0.1.3=pyhd8ed1ab_0 32 | - contourpy=1.1.0=py311he4fd1f5_0 33 | - cvxpy=1.3.2=py311ha1ab1f8_0 34 | - cvxpy-base=1.3.2=py311h9e438b8_0 35 | - cycler=0.11.0=pyhd8ed1ab_0 36 | - cython=3.0.0=py311ha891d26_0 37 | - debugpy=1.6.7=py311ha397e9f_0 38 | - decorator=5.1.1=pyhd8ed1ab_0 39 | - defusedxml=0.7.1=pyhd8ed1ab_0 40 | - ecos=2.0.11=py311h4add359_0 41 | - entrypoints=0.4=pyhd8ed1ab_0 42 | - exceptiongroup=1.1.2=pyhd8ed1ab_0 43 | - executing=1.2.0=pyhd8ed1ab_0 44 | - flake8=6.1.0=pyhd8ed1ab_0 45 | - flit-core=3.9.0=pyhd8ed1ab_0 46 | - fonttools=4.41.1=py311heffc1b2_0 47 | - freetype=2.12.1=hd633e50_1 48 | - gmp=6.2.1=h9f76cd9_0 49 | - gmpy2=2.1.2=py311h2ba9262_1 50 | - idna=3.4=pyhd8ed1ab_0 51 | - importlib-metadata=6.8.0=pyha770c72_0 52 | - importlib_metadata=6.8.0=hd8ed1ab_0 53 | - importlib_resources=6.0.0=pyhd8ed1ab_1 54 | - ipykernel=6.24.0=pyh5fb750a_0 55 | - ipython=8.14.0=pyhd1c38e8_0 56 | - ipywidgets=8.0.7=pyhd8ed1ab_0 57 | - jedi=0.18.2=pyhd8ed1ab_0 58 | - jinja2=3.1.2=pyhd8ed1ab_1 59 | - joblib=1.3.0=pyhd8ed1ab_1 60 | - json5=0.9.14=pyhd8ed1ab_0 61 | - jsonschema=4.18.4=pyhd8ed1ab_0 62 | - jsonschema-specifications=2023.7.1=pyhd8ed1ab_0 63 | - jupyter=1.0.0=py311h267d04e_8 64 | - jupyter-lsp=2.2.0=pyhd8ed1ab_0 65 | - jupyter_client=8.3.0=pyhd8ed1ab_0 66 | - jupyter_console=6.6.3=pyhd8ed1ab_0 67 | - jupyter_core=5.3.1=py311h267d04e_0 68 | - jupyter_events=0.6.3=pyhd8ed1ab_0 69 | - jupyter_server=2.7.0=pyhd8ed1ab_0 70 | - jupyter_server_terminals=0.4.4=pyhd8ed1ab_1 71 | - jupyterlab=4.0.3=pyhd8ed1ab_0 72 | - jupyterlab_pygments=0.2.2=pyhd8ed1ab_0 73 | - jupyterlab_server=2.24.0=pyhd8ed1ab_0 74 | - jupyterlab_widgets=3.0.8=pyhd8ed1ab_0 75 | - kiwisolver=1.4.4=py311hd6ee22a_1 76 | - lcms2=2.15=hd835a16_1 77 | - lerc=4.0.0=h9a09cb3_0 78 | - libblas=3.9.0=17_osxarm64_openblas 79 | - libbrotlicommon=1.0.9=h1a8c8d9_9 80 | - libbrotlidec=1.0.9=h1a8c8d9_9 81 | - libbrotlienc=1.0.9=h1a8c8d9_9 82 | - libcblas=3.9.0=17_osxarm64_openblas 83 | - libcxx=16.0.6=h4653b0c_0 84 | - libdeflate=1.18=h1a8c8d9_0 85 | - libexpat=2.5.0=hb7217d7_1 86 | - libffi=3.4.2=h3422bc3_5 87 | - libgfortran=5.0.0=12_3_0_hd922786_1 88 | - libgfortran5=12.3.0=ha3a6a3e_1 89 | - libjpeg-turbo=2.1.5.1=h1a8c8d9_0 90 | - liblapack=3.9.0=17_osxarm64_openblas 91 | - libopenblas=0.3.23=openmp_hc731615_0 92 | - libosqp=0.6.3=h13dd4ca_0 93 | - libpng=1.6.39=h76d750c_0 94 | - libqdldl=0.1.5=hb7217d7_1 95 | - libsodium=1.0.18=h27ca646_1 96 | - libsqlite=3.42.0=hb31c410_0 97 | - libtiff=4.5.1=h23a1a89_0 98 | - libwebp-base=1.3.1=hb547adb_0 99 | - libxcb=1.15=hf346824_0 100 | - libzlib=1.2.13=h53f4e23_5 101 | - llvm-openmp=16.0.6=h1c12783_0 102 | - markupsafe=2.1.3=py311heffc1b2_0 103 | - matplotlib=3.7.2=py311ha1ab1f8_0 104 | - matplotlib-base=3.7.2=py311h3bc9839_0 105 | - matplotlib-inline=0.1.6=pyhd8ed1ab_0 106 | - mccabe=0.7.0=pyhd8ed1ab_0 107 | - mistune=3.0.0=pyhd8ed1ab_0 108 | - mpc=1.3.1=h91ba8db_0 109 | - mpfr=4.2.0=he09a6ba_0 110 | - mpmath=1.3.0=pyhd8ed1ab_0 111 | - munkres=1.1.4=pyh9f0ad1d_0 112 | - mypy_extensions=1.0.0=pyha770c72_0 113 | - nbclient=0.8.0=pyhd8ed1ab_0 114 | - nbconvert=7.7.2=pyhd8ed1ab_0 115 | - nbconvert-core=7.7.2=pyhd8ed1ab_0 116 | - nbconvert-pandoc=7.7.2=pyhd8ed1ab_0 117 | - nbformat=5.9.1=pyhd8ed1ab_0 118 | - ncurses=6.4=h7ea286d_0 119 | - nest-asyncio=1.5.6=pyhd8ed1ab_0 120 | - notebook=7.0.0=pyhd8ed1ab_0 121 | - notebook-shim=0.2.3=pyhd8ed1ab_0 122 | - numpy=1.25.1=py311hb8f3215_0 123 | - openjpeg=2.5.0=hbc2ba62_2 124 | - openssl=3.1.2=h53f4e23_0 125 | - osqp=0.6.3=py311h9e438b8_1 126 | - overrides=7.3.1=pyhd8ed1ab_0 127 | - packaging=23.1=pyhd8ed1ab_0 128 | - pandas=2.0.3=py311h9e438b8_1 129 | - pandoc=3.1.3=hce30654_0 130 | - pandocfilters=1.5.0=pyhd8ed1ab_0 131 | - parso=0.8.3=pyhd8ed1ab_0 132 | - pathspec=0.11.2=pyhd8ed1ab_0 133 | - patsy=0.5.3=pyhd8ed1ab_0 134 | - pexpect=4.8.0=pyh1a96a4e_2 135 | - pickleshare=0.7.5=py_1003 136 | - pillow=10.0.0=py311h095fde6_0 137 | - pip=23.2.1=pyhd8ed1ab_0 138 | - pkgutil-resolve-name=1.3.10=pyhd8ed1ab_0 139 | - platformdirs=3.9.1=pyhd8ed1ab_0 140 | - pooch=1.7.0=pyha770c72_3 141 | - prometheus_client=0.17.1=pyhd8ed1ab_0 142 | - prompt-toolkit=3.0.39=pyha770c72_0 143 | - prompt_toolkit=3.0.39=hd8ed1ab_0 144 | - psutil=5.9.5=py311he2be06e_0 145 | - pthread-stubs=0.4=h27ca646_1001 146 | - ptyprocess=0.7.0=pyhd3deb0d_0 147 | - pure_eval=0.2.2=pyhd8ed1ab_0 148 | - pycodestyle=2.11.0=pyhd8ed1ab_0 149 | - pycparser=2.21=pyhd8ed1ab_0 150 | - pyflakes=3.1.0=pyhd8ed1ab_0 151 | - pygments=2.15.1=pyhd8ed1ab_0 152 | - pyobjc-core=9.2=py311hb702dc4_0 153 | - pyobjc-framework-cocoa=9.2=py311hb702dc4_0 154 | - pyparsing=3.0.9=pyhd8ed1ab_0 155 | - pysocks=1.7.1=pyha2e5f31_6 156 | - python=3.11.4=h47c9636_0_cpython 157 | - python-dateutil=2.8.2=pyhd8ed1ab_0 158 | - python-fastjsonschema=2.18.0=pyhd8ed1ab_0 159 | - python-json-logger=2.0.7=pyhd8ed1ab_0 160 | - python-tzdata=2023.3=pyhd8ed1ab_0 161 | - python_abi=3.11=3_cp311 162 | - pytz=2023.3=pyhd8ed1ab_0 163 | - pyyaml=6.0=py311he2be06e_5 164 | - pyzmq=25.1.0=py311hb1af645_0 165 | - qdldl-python=0.1.5.post2=py311h4eec4a9_0 166 | - readline=8.2=h92ec313_1 167 | - referencing=0.30.0=pyhd8ed1ab_0 168 | - requests=2.31.0=pyhd8ed1ab_0 169 | - rfc3339-validator=0.1.4=pyhd8ed1ab_0 170 | - rfc3986-validator=0.1.1=pyh9f0ad1d_0 171 | - rpds-py=0.9.2=py311h0563b04_0 172 | - scikit-learn=1.3.0=py311hf0b18b8_0 173 | - scipy=1.11.1=py311h93d07a4_0 174 | - scs=3.2.3=py311h0dc035f_0 175 | - seaborn=0.12.2=hd8ed1ab_0 176 | - seaborn-base=0.12.2=pyhd8ed1ab_0 177 | - send2trash=1.8.2=pyhd1c38e8_0 178 | - setuptools=68.0.0=pyhd8ed1ab_0 179 | - six=1.16.0=pyh6c4a22f_0 180 | - sniffio=1.3.0=pyhd8ed1ab_0 181 | - soupsieve=2.3.2.post1=pyhd8ed1ab_0 182 | - stack_data=0.6.2=pyhd8ed1ab_0 183 | - statsmodels=0.14.0=py311hb49d859_1 184 | - sympy=1.12=pypyh9d50eac_103 185 | - terminado=0.17.1=pyhd1c38e8_0 186 | - threadpoolctl=3.2.0=pyha21a80b_0 187 | - tinycss2=1.2.1=pyhd8ed1ab_0 188 | - tk=8.6.12=he1e0b03_0 189 | - tokenize-rt=5.2.0=pyhd8ed1ab_0 190 | - tomli=2.0.1=pyhd8ed1ab_0 191 | - tornado=6.3.2=py311heffc1b2_0 192 | - traitlets=5.9.0=pyhd8ed1ab_0 193 | - typing-extensions=4.7.1=hd8ed1ab_0 194 | - typing_extensions=4.7.1=pyha770c72_0 195 | - typing_utils=0.1.0=pyhd8ed1ab_0 196 | - tzdata=2023c=h71feb2d_0 197 | - urllib3=2.0.4=pyhd8ed1ab_0 198 | - wcwidth=0.2.6=pyhd8ed1ab_0 199 | - webencodings=0.5.1=py_1 200 | - websocket-client=1.6.1=pyhd8ed1ab_0 201 | - wheel=0.41.0=pyhd8ed1ab_0 202 | - widgetsnbextension=4.0.8=pyhd8ed1ab_0 203 | - xorg-libxau=1.0.11=hb547adb_0 204 | - xorg-libxdmcp=1.1.3=h27ca646_0 205 | - xz=5.2.6=h57fd34a_0 206 | - yaml=0.2.5=h3422bc3_2 207 | - zeromq=4.3.4=hbdafb3b_1 208 | - zipp=3.16.2=pyhd8ed1ab_0 209 | - zstd=1.5.2=h4f39d0f_7 210 | prefix: /opt/anaconda3/envs/FMNM 211 | -------------------------------------------------------------------------------- /latex/A.3 Introduction to Lévy processes and PIDEs.bbl: -------------------------------------------------------------------------------- 1 | \begin{thebibliography}{} 2 | 3 | \bibitem[Applebaum, 2009]{Applebaum} 4 | Applebaum, D. (2009). 5 | \newblock {\em Lévy Processes and Stochastic Calculus}. 6 | \newblock Cambridge University Press; 2nd edition. 7 | 8 | \bibitem[Barndorff-Nielsen, 1997]{BN97} 9 | Barndorff-Nielsen (1997). 10 | \newblock Processes of {N}ormal inverse {G}aussian type. 11 | \newblock {\em Finance and Stochastics}, 2:41--68. 12 | 13 | \bibitem[Black and Scholes, 1973]{BS73} 14 | Black, F. and Scholes, M. (1973). 15 | \newblock The pricing of options and corporate liabilities. 16 | \newblock {\em The Journal of Political Economy}, 81(3):637--654. 17 | 18 | \bibitem[Carr et~al., 2002]{CGMY02} 19 | Carr, P., Geman, H., D.B., M., and M., Y. (2002). 20 | \newblock The fine structure of asset returns: An empirical investigation. 21 | \newblock {\em Journal of Business}, 75(2):305--333. 22 | 23 | \bibitem[Cont et~al., 1997]{BoPoCo97} 24 | Cont, R., Potters, M., and Bouchaud, J. (1997). 25 | \newblock Scaling in stock market data: stable laws and beyond. 26 | \newblock {\em Scale invariance and beyond, Springer}. 27 | 28 | \bibitem[Cont and Tankov, 2003]{Cont} 29 | Cont, R. and Tankov, P. (2003). 30 | \newblock {\em Financial Modelling with Jump Processes}. 31 | \newblock Chapman and Hall/CRC; 1 edition. 32 | 33 | \bibitem[Cont and Voltchkova, 2005a]{CoVo05b} 34 | Cont, R. and Voltchkova, E. (2005a). 35 | \newblock A finite difference scheme for option pricing in jump diffusion and 36 | exponential {L}\'evy models. 37 | \newblock {\em SIAM Journal of numerical analysis}, 43(4):1596--1626. 38 | 39 | \bibitem[Cont and Voltchkova, 2005b]{CoVo05} 40 | Cont, R. and Voltchkova, E. (2005b). 41 | \newblock Integro-differential equations for option prices in exponential 42 | {L}èvy models. 43 | \newblock {\em Finance and Stochastics}, 9:299--325. 44 | 45 | \bibitem[Eberlein and Keller, 1995]{EbKe95} 46 | Eberlein, E. and Keller, U. (1995). 47 | \newblock Hyperbolic distributions in finance. 48 | \newblock {\em Bernoulli}, 1(3):281--299. 49 | 50 | \bibitem[Kabasinskas et~al., 2009]{alpha09} 51 | Kabasinskas, A., Rachev, S., Sakalauskas, L., Wei, S., and Belovas, I. (2009). 52 | \newblock Alpha-stable paradigm in financial markets. 53 | \newblock {\em Journal of Computational Analysis and Applications}, 54 | 11(4):641--669. 55 | 56 | \bibitem[Kou, 2002]{Kou02} 57 | Kou, S. (2002). 58 | \newblock A jump-diffusion model for option pricing. 59 | \newblock {\em Management Science}, 48(8):1086--1101. 60 | 61 | \bibitem[Madan et~al., 1998]{MCC98} 62 | Madan, D., Carr, P., and Chang, E. (1998). 63 | \newblock The {V}ariance {G}amma process and option pricing. 64 | \newblock {\em European Finance Review}, 2:79–105. 65 | 66 | \bibitem[Madan and Seneta, 1990]{MaSe90} 67 | Madan, D. and Seneta, E. (1990). 68 | \newblock The {V}ariance {G}amma {(V.G.)} model for share market returns. 69 | \newblock {\em The journal of Business}, 63(4):511--524. 70 | 71 | \bibitem[Mandelbrot, 1963]{Ma63} 72 | Mandelbrot, B. (1963). 73 | \newblock Modeling financial data with stable distributions. 74 | \newblock {\em Journal of Business}, XXXVI(1):392--417. 75 | 76 | \bibitem[Merton, 1976]{Me76} 77 | Merton, R. (1976). 78 | \newblock Option pricing when underlying stock returns are discontinuous. 79 | \newblock {\em Journal of Financial Economics}, 3:125--144. 80 | 81 | \bibitem[Papapantoleon, ]{papapa} 82 | Papapantoleon, A. 83 | \newblock An introduction to lévy processes with applications in finance. 84 | \newblock {\em Available in Arxiv}. 85 | 86 | \bibitem[Sato, 1999]{Sato} 87 | Sato, K.~I. (1999). 88 | \newblock {\em Lévy processes and infinitely divisible distributions}. 89 | \newblock Cambridge University Press. 90 | 91 | \bibitem[Schoutens, 2003]{Schoutens} 92 | Schoutens, W. (2003). 93 | \newblock {\em L\'evy processes in finance}. 94 | \newblock Wiley, First Edition. 95 | 96 | \end{thebibliography} 97 | -------------------------------------------------------------------------------- /list_of_packages.txt: -------------------------------------------------------------------------------- 1 | arch 2 | cvxpy 3 | cython 4 | jupyter 5 | matplotlib 6 | numpy 7 | pandas 8 | scikit-learn 9 | scipy 10 | seaborn 11 | statsmodels 12 | sympy 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm", "numpy", "cython"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "FMNM" 7 | version = "1.0.0" 8 | authors = [{ name = "cantaro86", email = "nicolacantarutti@gmail.com" }] 9 | description = "Library for the code used in the FMNM notebooks." 10 | readme = "README.md" 11 | requires-python = ">=3.8" 12 | dynamic = ["dependencies"] 13 | 14 | [tool.setuptools.dynamic] 15 | dependencies = {file = ["list_of_packages.txt"]} 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | namespaces = false # prevent folders without __init__.py from being scanned 20 | 21 | [project.urls] 22 | "Homepage" = "https://github.com/cantaro86/Financial-Models-Numerical-Methods" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.7.1 2 | appnope==0.1.3 3 | arch==6.1.0 4 | argon2-cffi==21.3.0 5 | argon2-cffi-bindings==21.2.0 6 | arrow==1.2.3 7 | asttokens==2.2.1 8 | async-lru==2.0.3 9 | attrs==23.1.0 10 | Babel==2.12.1 11 | backcall==0.2.0 12 | beautifulsoup4==4.12.2 13 | black==23.7.0 14 | bleach==6.0.0 15 | certifi==2023.7.22 16 | cffi==1.15.1 17 | charset-normalizer==3.2.0 18 | click==8.1.6 19 | comm==0.1.3 20 | contourpy==1.1.0 21 | cvxpy==1.3.2 22 | cycler==0.11.0 23 | Cython==3.0.0 24 | debugpy==1.6.7 25 | decorator==5.1.1 26 | defusedxml==0.7.1 27 | ecos==2.0.12 28 | executing==1.2.0 29 | fastjsonschema==2.18.0 30 | flake8==6.0.0 31 | fonttools==4.41.1 32 | fqdn==1.5.1 33 | idna==3.4 34 | ipykernel==6.24.0 35 | ipython==8.14.0 36 | ipython-genutils==0.2.0 37 | ipywidgets==8.0.7 38 | isoduration==20.11.0 39 | jedi==0.18.2 40 | Jinja2==3.1.2 41 | joblib==1.3.1 42 | json5==0.9.14 43 | jsonpointer==2.4 44 | jsonschema==4.18.4 45 | jsonschema-specifications==2023.7.1 46 | jupyter==1.0.0 47 | jupyter-console==6.6.3 48 | jupyter-events==0.6.3 49 | jupyter-lsp==2.2.0 50 | jupyter_client==8.3.0 51 | jupyter_core==5.3.1 52 | jupyter_server==2.7.0 53 | jupyter_server_terminals==0.4.4 54 | jupyterlab==4.0.3 55 | jupyterlab-pygments==0.2.2 56 | jupyterlab-widgets==3.0.8 57 | jupyterlab_server==2.24.0 58 | kiwisolver==1.4.4 59 | MarkupSafe==2.1.3 60 | matplotlib==3.7.2 61 | matplotlib-inline==0.1.6 62 | mccabe==0.7.0 63 | mistune==3.0.1 64 | mpmath==1.3.0 65 | mypy-extensions==1.0.0 66 | nbclient==0.8.0 67 | nbconvert==7.7.2 68 | nbformat==5.9.1 69 | nest-asyncio==1.5.6 70 | notebook==7.0.0 71 | notebook_shim==0.2.3 72 | numpy==1.25.1 73 | osqp==0.6.3 74 | overrides==7.3.1 75 | packaging==23.1 76 | pandas==2.0.3 77 | pandocfilters==1.5.0 78 | parso==0.8.3 79 | pathspec==0.11.1 80 | patsy==0.5.3 81 | pexpect==4.8.0 82 | pickleshare==0.7.5 83 | Pillow==10.0.0 84 | platformdirs==3.9.1 85 | prometheus-client==0.17.1 86 | prompt-toolkit==3.0.39 87 | psutil==5.9.5 88 | ptyprocess==0.7.0 89 | pure-eval==0.2.2 90 | pycodestyle==2.10.0 91 | pycparser==2.21 92 | pyflakes==3.0.1 93 | Pygments==2.15.1 94 | pyparsing==3.0.9 95 | python-dateutil==2.8.2 96 | python-json-logger==2.0.7 97 | pytz==2023.3 98 | PyYAML==6.0.1 99 | pyzmq==25.1.0 100 | qdldl==0.1.7.post0 101 | qtconsole==5.4.3 102 | QtPy==2.3.1 103 | referencing==0.30.0 104 | requests==2.31.0 105 | rfc3339-validator==0.1.4 106 | rfc3986-validator==0.1.1 107 | rpds-py==0.9.2 108 | scikit-learn==1.3.0 109 | scipy==1.11.1 110 | scs==3.2.3 111 | seaborn==0.12.2 112 | Send2Trash==1.8.2 113 | six==1.16.0 114 | sniffio==1.3.0 115 | soupsieve==2.4.1 116 | stack-data==0.6.2 117 | statsmodels==0.14.0 118 | sympy==1.12 119 | terminado==0.17.1 120 | threadpoolctl==3.2.0 121 | tinycss2==1.2.1 122 | tokenize-rt==5.1.0 123 | tornado==6.3.2 124 | traitlets==5.9.0 125 | tzdata==2023.3 126 | uri-template==1.3.0 127 | urllib3==2.0.4 128 | wcwidth==0.2.6 129 | webcolors==1.13 130 | webencodings==0.5.1 131 | websocket-client==1.6.1 132 | widgetsnbextension==4.0.8 133 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Fri Jul 29 17:49:22 2023 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | 10 | from setuptools import Extension, setup 11 | from Cython.Build import build_ext, cythonize 12 | import numpy 13 | 14 | extensions = [ 15 | Extension( 16 | "src/FMNM/cython/*", 17 | ["src/FMNM/cython/*.pyx"], 18 | include_dirs=[numpy.get_include()], 19 | define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")], 20 | ) 21 | ] 22 | 23 | 24 | setup( 25 | name="fmnm_cython", 26 | cmdclass={"build_ext": build_ext}, 27 | ext_modules=cythonize(extensions, language_level="3"), 28 | ) 29 | -------------------------------------------------------------------------------- /src/C/BS_SOR_main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "SOR.h" 5 | #include "PDE_solver.h" 6 | #include 7 | 8 | 9 | 10 | 11 | int main() 12 | { 13 | 14 | double r = 0.1; 15 | double sig = 0.2; 16 | double S = 100.0; 17 | double K = 100.0; 18 | double T = 1.; 19 | 20 | int Nspace = 3000; // space steps 21 | int Ntime = 2000; // time steps 22 | 23 | double w = 1.68; // relaxation parameter 24 | 25 | printf("The price is: %f \n ", PDE_SOR(Nspace,Ntime,S,K,T,sig,r,w) ); 26 | 27 | return 0; 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/C/BS_sor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantaro86/Financial-Models-Numerical-Methods/585ed19462665bd822c88b5b6e34fca8ad441370/src/C/BS_sor -------------------------------------------------------------------------------- /src/C/Makefile: -------------------------------------------------------------------------------- 1 | PROGRAM_NAME=BS_sor 2 | OBJECTS=BS_SOR_main.o SOR.o PDE_solver.o 3 | 4 | CXX=clang #gcc 5 | 6 | 7 | CXXFLAGS +=-Wall -Werror -O2 8 | LIBS=-lm 9 | 10 | 11 | 12 | 13 | $(PROGRAM_NAME):$(OBJECTS) 14 | $(CXX) $(CXXFLAGS) -o $(PROGRAM_NAME) $(OBJECTS) $(LIBS) 15 | 16 | @echo " " 17 | @echo "Compilation completed!" 18 | @echo " " 19 | 20 | 21 | 22 | BS_SOR_main.o:SOR.h PDE_solver.h BS_SOR_main.c 23 | $(CXX) $(CXXFLAGS) -c -o BS_SOR_main.o BS_SOR_main.c 24 | 25 | 26 | SOR.o:SOR.h SOR.c 27 | $(CXX) $(CXXFLAGS) -c -o SOR.o SOR.c 28 | 29 | 30 | PDE_solver.o:SOR.h SOR.c PDE_solver.h PDE_solver.c 31 | $(CXX) $(CXXFLAGS) -c -o PDE_solver.o PDE_solver.c 32 | 33 | 34 | 35 | clean: 36 | rm -f *.o 37 | rm -f *~ 38 | rm -f core 39 | 40 | 41 | cat: 42 | cat Makefile 43 | 44 | 45 | all: $(PROGRAM_NAME) clean 46 | -------------------------------------------------------------------------------- /src/C/PDE_solver.c: -------------------------------------------------------------------------------- 1 | #include "PDE_solver.h" 2 | 3 | 4 | 5 | 6 | double PDE_SOR(int Ns, int Nt, double S, double K, double T, double sig, double r, double w) 7 | { 8 | 9 | const double eps = 1e-10; 10 | const int N_max = 600; 11 | 12 | double S_max = 3*K; 13 | double S_min = K/3; 14 | double x_max = log(S_max); 15 | double x_min = log(S_min); 16 | 17 | double dx = (x_max - x_min)/(Ns-1); 18 | double dt = T/Nt; 19 | 20 | double sig2 = sig*sig; 21 | double dxx = dx * dx; 22 | double aa = ( (dt/2) * ( (r-0.5*sig2)/dx - sig2/dxx ) ); 23 | double bb = ( 1 + dt * ( sig2/dxx + r ) ); 24 | double cc = (-(dt/2) * ( (r-0.5*sig2)/dx + sig2/dxx ) ); 25 | 26 | 27 | // array allocations 28 | double *x = calloc(Ns, sizeof(double) ); 29 | double *x_old = calloc(Ns-2, sizeof(double) ); 30 | double *x_new = calloc(Ns-2, sizeof(double) ); 31 | double *help_ptr = calloc(Ns-2, sizeof(double) ); 32 | double *temp; 33 | 34 | for (unsigned int i=0; i=0; --k) 43 | { 44 | x_old[Ns-3] -= cc * ( S_max - K * exp( -r*(T-k*dt) ) ); // offset 45 | x_new = SOR_aabbcc(aa, bb, cc, x_old, help_ptr, x_new, Ns-2, w, eps, N_max); //SOR solver 46 | // x_new = SOR_abc(aa, bb, cc, x_old, Ns-2, w, eps, N_max); //SOR solver 47 | 48 | if (k != 0) // swap the pointers (we don't need to allocate new memory) 49 | { 50 | temp = x_old; 51 | x_old = x_new; 52 | x_new = temp; 53 | } 54 | } 55 | free(help_ptr); 56 | free(x_old); 57 | 58 | // x_new is the solution!! 59 | 60 | // binary search: Search for the points for the interpolation 61 | 62 | int low = 1; 63 | int high = Ns-2; 64 | int mid; 65 | double result = -1; 66 | 67 | if (S > x[high] || S < x[low]) 68 | { 69 | printf("error: Price S out of grid.\n"); 70 | free(x_new); 71 | free(x); 72 | return result; 73 | } 74 | 75 | while ( (low+1) != high) 76 | { 77 | 78 | mid = (low + high) / 2; 79 | 80 | if ( fabs(x[mid]-S)< 1e-10 ) 81 | { 82 | result = x_new[mid-1]; 83 | free(x_new); 84 | free(x); 85 | return result; 86 | } 87 | else if ( x[mid] < S) 88 | { 89 | low = mid; 90 | } 91 | else 92 | { 93 | high = mid; 94 | } 95 | } 96 | 97 | // linear interpolation 98 | result = x_new[low-1] + (S - x[low]) * (x_new[high-1] - x_new[low-1]) / (x[high] - x[low]) ; 99 | free(x_new); 100 | free(x); 101 | return result; 102 | } 103 | -------------------------------------------------------------------------------- /src/C/PDE_solver.h: -------------------------------------------------------------------------------- 1 | #ifndef PDE_SOLVER_H 2 | #define PDE_SOLVER_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include "SOR.h" 9 | #include 10 | 11 | double PDE_SOR(int Ns, int Nt, double S, double K, double T, double sig, double r, double w); 12 | 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /src/C/SOR.c: -------------------------------------------------------------------------------- 1 | #include "SOR.h" 2 | 3 | 4 | 5 | double dist_square(double* a, double* b) 6 | { 7 | double dist = 0; 8 | 9 | for (int i=0; i 6 | #include 7 | #include 8 | 9 | 10 | static const int N = 4; 11 | 12 | void print_matrix(double arr[N][N]); 13 | void print_array(double* arr); 14 | 15 | double* SOR(double A[N][N], double* b, double w, double eps, int N_max); 16 | double* SOR_trid(double A[N][N], double* b, double w, double eps, int N_max); 17 | double* SOR_abc(double aa, double bb, double cc, double* b, int NN, double w, double eps, int N_max); 18 | double* SOR_aabbcc(double aa, double bb, double cc, double *b, double *x0, double *x1, 19 | int NN, double w, double eps, int N_max); 20 | 21 | 22 | double dist_square(double* a, double* b); 23 | double dist_square2(double* a, double* b, int NN); 24 | void print_array_2(double* arr, int NN); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /src/C/mainSOR.c: -------------------------------------------------------------------------------- 1 | #include "SOR.h" 2 | 3 | 4 | 5 | /* 6 | GENERAL MATRIX (DIAGONAL DOMINANT) 7 | 8 | double A[4][4] = { {10, 5, 2, 1}, 9 | {2, 15, 2, 3}, 10 | {1, 8, 13, 1}, 11 | {2, 3, 1, 8} }; 12 | double b[4] = {30, 50, 60, 43}; 13 | double* x = SOR(A,b, w,eps, N_max); 14 | 15 | TRIDIAGONAL MATRIX 16 | double A[4][4] = { {10, 5, 0, 0}, 17 | {2, 15, 2, 0}, 18 | {0, 8, 13, 1}, 19 | {0, 0, 1, 8} }; 20 | double b[4] = {20, 38, 59, 35}; 21 | double* x = SOR_trid(A, b, w, eps, N_max); 22 | */ 23 | 24 | 25 | 26 | int main() 27 | { 28 | // TRIDIAGONAL WITH CONSTANT aa,bb,cc 29 | double A[4][4] = { {10, 5, 0, 0}, 30 | {2, 10, 5, 0}, 31 | {0, 2, 10, 5}, 32 | {0, 0, 2, 10} }; 33 | 34 | double b[4] = {20, 37, 54, 46}; 35 | 36 | double aa=2, bb=10, cc=5; 37 | 38 | 39 | const double w = 1; 40 | const double eps = 1e-10; 41 | const int N_max = 100; 42 | 43 | 44 | printf("Matrix A: \n"); 45 | print_matrix(A); 46 | printf("Vector b: \n"); 47 | print_array(b); 48 | 49 | // double* x = SOR_abc(aa, bb, cc, b, N, w, eps, N_max); 50 | 51 | double* x0 = calloc(N, sizeof(double) ); 52 | double* x1 = calloc(N, sizeof(double) ); 53 | double* x = SOR_aabbcc(aa, bb, cc, b, x0, x1, N, w, eps, N_max); 54 | 55 | printf("Solution x: \n"); 56 | print_array(x); 57 | 58 | free(x0); 59 | free(x1); 60 | 61 | return 0; 62 | } 63 | -------------------------------------------------------------------------------- /src/FMNM/BS_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Thu Jun 13 10:18:39 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | import scipy as scp 11 | from scipy.sparse.linalg import spsolve 12 | from scipy import sparse 13 | from scipy.sparse.linalg import splu 14 | import matplotlib.pyplot as plt 15 | from matplotlib import cm 16 | from time import time 17 | import scipy.stats as ss 18 | from FMNM.Solvers import Thomas 19 | from FMNM.cython.solvers import SOR 20 | from FMNM.CF import cf_normal 21 | from FMNM.probabilities import Q1, Q2 22 | from functools import partial 23 | from FMNM.FFT import fft_Lewis, IV_from_Lewis 24 | 25 | 26 | class BS_pricer: 27 | """ 28 | Closed Formula. 29 | Monte Carlo. 30 | Finite-difference Black-Scholes PDE: 31 | df/dt + r df/dx + 1/2 sigma^2 d^f/dx^2 -rf = 0 32 | """ 33 | 34 | def __init__(self, Option_info, Process_info): 35 | """ 36 | Option_info: of type Option_param. It contains (S0,K,T) 37 | i.e. current price, strike, maturity in years 38 | Process_info: of type Diffusion_process. It contains (r, mu, sig) i.e. 39 | interest rate, drift coefficient, diffusion coefficient 40 | """ 41 | self.r = Process_info.r # interest rate 42 | self.sig = Process_info.sig # diffusion coefficient 43 | self.S0 = Option_info.S0 # current price 44 | self.K = Option_info.K # strike 45 | self.T = Option_info.T # maturity in years 46 | self.exp_RV = Process_info.exp_RV # function to generate solution of GBM 47 | 48 | self.price = 0 49 | self.S_vec = None 50 | self.price_vec = None 51 | self.mesh = None 52 | self.exercise = Option_info.exercise 53 | self.payoff = Option_info.payoff 54 | 55 | def payoff_f(self, S): 56 | if self.payoff == "call": 57 | Payoff = np.maximum(S - self.K, 0) 58 | elif self.payoff == "put": 59 | Payoff = np.maximum(self.K - S, 0) 60 | return Payoff 61 | 62 | @staticmethod 63 | def BlackScholes(payoff="call", S0=100.0, K=100.0, T=1.0, r=0.1, sigma=0.2): 64 | """Black Scholes closed formula: 65 | payoff: call or put. 66 | S0: float. initial stock/index level. 67 | K: float strike price. 68 | T: float maturity (in year fractions). 69 | r: float constant risk-free short rate. 70 | sigma: volatility factor in diffusion term.""" 71 | 72 | d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T)) 73 | d2 = (np.log(S0 / K) + (r - sigma**2 / 2) * T) / (sigma * np.sqrt(T)) 74 | 75 | if payoff == "call": 76 | return S0 * ss.norm.cdf(d1) - K * np.exp(-r * T) * ss.norm.cdf(d2) 77 | elif payoff == "put": 78 | return K * np.exp(-r * T) * ss.norm.cdf(-d2) - S0 * ss.norm.cdf(-d1) 79 | else: 80 | raise ValueError("invalid type. Set 'call' or 'put'") 81 | 82 | @staticmethod 83 | def vega(sigma, S0, K, T, r): 84 | """BS vega: derivative of the price with respect to the volatility""" 85 | d1 = (np.log(S0 / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T)) 86 | return S0 * np.sqrt(T) * ss.norm.pdf(d1) 87 | 88 | def closed_formula(self): 89 | """ 90 | Black Scholes closed formula: 91 | """ 92 | d1 = (np.log(self.S0 / self.K) + (self.r + self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T)) 93 | d2 = (np.log(self.S0 / self.K) + (self.r - self.sig**2 / 2) * self.T) / (self.sig * np.sqrt(self.T)) 94 | 95 | if self.payoff == "call": 96 | return self.S0 * ss.norm.cdf(d1) - self.K * np.exp(-self.r * self.T) * ss.norm.cdf(d2) 97 | elif self.payoff == "put": 98 | return self.K * np.exp(-self.r * self.T) * ss.norm.cdf(-d2) - self.S0 * ss.norm.cdf(-d1) 99 | else: 100 | raise ValueError("invalid type. Set 'call' or 'put'") 101 | 102 | def Fourier_inversion(self): 103 | """ 104 | Price obtained by inversion of the characteristic function 105 | """ 106 | k = np.log(self.K / self.S0) 107 | cf_GBM = partial( 108 | cf_normal, 109 | mu=(self.r - 0.5 * self.sig**2) * self.T, 110 | sig=self.sig * np.sqrt(self.T), 111 | ) # function binding 112 | 113 | if self.payoff == "call": 114 | call = self.S0 * Q1(k, cf_GBM, np.inf) - self.K * np.exp(-self.r * self.T) * Q2( 115 | k, cf_GBM, np.inf 116 | ) # pricing function 117 | return call 118 | elif self.payoff == "put": 119 | put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_GBM, np.inf)) - self.S0 * ( 120 | 1 - Q1(k, cf_GBM, np.inf) 121 | ) # pricing function 122 | return put 123 | else: 124 | raise ValueError("invalid type. Set 'call' or 'put'") 125 | 126 | def FFT(self, K): 127 | """ 128 | FFT method. It returns a vector of prices. 129 | K is an array of strikes 130 | """ 131 | K = np.array(K) 132 | cf_GBM = partial( 133 | cf_normal, 134 | mu=(self.r - 0.5 * self.sig**2) * self.T, 135 | sig=self.sig * np.sqrt(self.T), 136 | ) # function binding 137 | if self.payoff == "call": 138 | return fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic") 139 | elif self.payoff == "put": # put-call parity 140 | return ( 141 | fft_Lewis(K, self.S0, self.r, self.T, cf_GBM, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T) 142 | ) 143 | else: 144 | raise ValueError("invalid type. Set 'call' or 'put'") 145 | 146 | def IV_Lewis(self): 147 | """Implied Volatility from the Lewis formula""" 148 | 149 | cf_GBM = partial( 150 | cf_normal, 151 | mu=(self.r - 0.5 * self.sig**2) * self.T, 152 | sig=self.sig * np.sqrt(self.T), 153 | ) # function binding 154 | if self.payoff == "call": 155 | return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_GBM) 156 | elif self.payoff == "put": 157 | raise NotImplementedError 158 | else: 159 | raise ValueError("invalid type. Set 'call' or 'put'") 160 | 161 | def MC(self, N, Err=False, Time=False): 162 | """ 163 | BS Monte Carlo 164 | Err = return Standard Error if True 165 | Time = return execution time if True 166 | """ 167 | t_init = time() 168 | 169 | S_T = self.exp_RV(self.S0, self.T, N) 170 | PayOff = self.payoff_f(S_T) 171 | V = scp.mean(np.exp(-self.r * self.T) * PayOff, axis=0) 172 | 173 | if Err is True: 174 | if Time is True: 175 | elapsed = time() - t_init 176 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed 177 | else: 178 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)) 179 | else: 180 | if Time is True: 181 | elapsed = time() - t_init 182 | return V, elapsed 183 | else: 184 | return V 185 | 186 | def PDE_price(self, steps, Time=False, solver="splu"): 187 | """ 188 | steps = tuple with number of space steps and time steps 189 | payoff = "call" or "put" 190 | exercise = "European" or "American" 191 | Time = Boolean. Execution time. 192 | Solver = spsolve or splu or Thomas or SOR 193 | """ 194 | t_init = time() 195 | 196 | Nspace = steps[0] 197 | Ntime = steps[1] 198 | 199 | S_max = 6 * float(self.K) 200 | S_min = float(self.K) / 6 201 | x_max = np.log(S_max) 202 | x_min = np.log(S_min) 203 | x0 = np.log(self.S0) # current log-price 204 | 205 | x, dx = np.linspace(x_min, x_max, Nspace, retstep=True) 206 | t, dt = np.linspace(0, self.T, Ntime, retstep=True) 207 | 208 | self.S_vec = np.exp(x) # vector of S 209 | Payoff = self.payoff_f(self.S_vec) 210 | 211 | V = np.zeros((Nspace, Ntime)) 212 | if self.payoff == "call": 213 | V[:, -1] = Payoff 214 | V[-1, :] = np.exp(x_max) - self.K * np.exp(-self.r * t[::-1]) 215 | V[0, :] = 0 216 | else: 217 | V[:, -1] = Payoff 218 | V[-1, :] = 0 219 | V[0, :] = Payoff[0] * np.exp(-self.r * t[::-1]) # Instead of Payoff[0] I could use K 220 | # For s to 0, the limiting value is e^(-rT)(K-s) 221 | 222 | sig2 = self.sig**2 223 | dxx = dx**2 224 | a = (dt / 2) * ((self.r - 0.5 * sig2) / dx - sig2 / dxx) 225 | b = 1 + dt * (sig2 / dxx + self.r) 226 | c = -(dt / 2) * ((self.r - 0.5 * sig2) / dx + sig2 / dxx) 227 | 228 | D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc() 229 | 230 | offset = np.zeros(Nspace - 2) 231 | 232 | if solver == "spsolve": 233 | if self.exercise == "European": 234 | for i in range(Ntime - 2, -1, -1): 235 | offset[0] = a * V[0, i] 236 | offset[-1] = c * V[-1, i] 237 | V[1:-1, i] = spsolve(D, (V[1:-1, i + 1] - offset)) 238 | elif self.exercise == "American": 239 | for i in range(Ntime - 2, -1, -1): 240 | offset[0] = a * V[0, i] 241 | offset[-1] = c * V[-1, i] 242 | V[1:-1, i] = np.maximum(spsolve(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1]) 243 | elif solver == "Thomas": 244 | if self.exercise == "European": 245 | for i in range(Ntime - 2, -1, -1): 246 | offset[0] = a * V[0, i] 247 | offset[-1] = c * V[-1, i] 248 | V[1:-1, i] = Thomas(D, (V[1:-1, i + 1] - offset)) 249 | elif self.exercise == "American": 250 | for i in range(Ntime - 2, -1, -1): 251 | offset[0] = a * V[0, i] 252 | offset[-1] = c * V[-1, i] 253 | V[1:-1, i] = np.maximum(Thomas(D, (V[1:-1, i + 1] - offset)), Payoff[1:-1]) 254 | elif solver == "SOR": 255 | if self.exercise == "European": 256 | for i in range(Ntime - 2, -1, -1): 257 | offset[0] = a * V[0, i] 258 | offset[-1] = c * V[-1, i] 259 | V[1:-1, i] = SOR(a, b, c, (V[1:-1, i + 1] - offset), w=1.68, eps=1e-10, N_max=600) 260 | elif self.exercise == "American": 261 | for i in range(Ntime - 2, -1, -1): 262 | offset[0] = a * V[0, i] 263 | offset[-1] = c * V[-1, i] 264 | V[1:-1, i] = np.maximum( 265 | SOR( 266 | a, 267 | b, 268 | c, 269 | (V[1:-1, i + 1] - offset), 270 | w=1.68, 271 | eps=1e-10, 272 | N_max=600, 273 | ), 274 | Payoff[1:-1], 275 | ) 276 | elif solver == "splu": 277 | DD = splu(D) 278 | if self.exercise == "European": 279 | for i in range(Ntime - 2, -1, -1): 280 | offset[0] = a * V[0, i] 281 | offset[-1] = c * V[-1, i] 282 | V[1:-1, i] = DD.solve(V[1:-1, i + 1] - offset) 283 | elif self.exercise == "American": 284 | for i in range(Ntime - 2, -1, -1): 285 | offset[0] = a * V[0, i] 286 | offset[-1] = c * V[-1, i] 287 | V[1:-1, i] = np.maximum(DD.solve(V[1:-1, i + 1] - offset), Payoff[1:-1]) 288 | else: 289 | raise ValueError("Solver is splu, spsolve, SOR or Thomas") 290 | 291 | self.price = np.interp(x0, x, V[:, 0]) 292 | self.price_vec = V[:, 0] 293 | self.mesh = V 294 | 295 | if Time is True: 296 | elapsed = time() - t_init 297 | return self.price, elapsed 298 | else: 299 | return self.price 300 | 301 | def plot(self, axis=None): 302 | if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray: 303 | self.PDE_price((7000, 5000)) 304 | # print("run the PDE_price method") 305 | # return 306 | 307 | plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff") 308 | plt.plot(self.S_vec, self.price_vec, color="red", label="BS curve") 309 | if type(axis) == list: 310 | plt.axis(axis) 311 | plt.xlabel("S") 312 | plt.ylabel("price") 313 | plt.title(f"{self.exercise} - Black Scholes price") 314 | plt.legend() 315 | plt.show() 316 | 317 | def mesh_plt(self): 318 | if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray: 319 | self.PDE_price((7000, 5000)) 320 | 321 | fig = plt.figure() 322 | ax = fig.add_subplot(111, projection="3d") 323 | 324 | X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec) 325 | ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean) 326 | ax.set_title(f"{self.exercise} - BS price surface") 327 | ax.set_xlabel("S") 328 | ax.set_ylabel("t") 329 | ax.set_zlabel("V") 330 | ax.view_init(30, -100) # this function rotates the 3d plot 331 | plt.show() 332 | 333 | def LSM(self, N=10000, paths=10000, order=2): 334 | """ 335 | Longstaff-Schwartz Method for pricing American options 336 | 337 | N = number of time steps 338 | paths = number of generated paths 339 | order = order of the polynomial for the regression 340 | """ 341 | 342 | if self.payoff != "put": 343 | raise ValueError("invalid type. Set 'call' or 'put'") 344 | 345 | dt = self.T / (N - 1) # time interval 346 | df = np.exp(-self.r * dt) # discount factor per time time interval 347 | 348 | X0 = np.zeros((paths, 1)) 349 | increments = ss.norm.rvs( 350 | loc=(self.r - self.sig**2 / 2) * dt, 351 | scale=np.sqrt(dt) * self.sig, 352 | size=(paths, N - 1), 353 | ) 354 | X = np.concatenate((X0, increments), axis=1).cumsum(1) 355 | S = self.S0 * np.exp(X) 356 | 357 | H = np.maximum(self.K - S, 0) # intrinsic values for put option 358 | V = np.zeros_like(H) # value matrix 359 | V[:, -1] = H[:, -1] 360 | 361 | # Valuation by LS Method 362 | for t in range(N - 2, 0, -1): 363 | good_paths = H[:, t] > 0 364 | rg = np.polyfit(S[good_paths, t], V[good_paths, t + 1] * df, 2) # polynomial regression 365 | C = np.polyval(rg, S[good_paths, t]) # evaluation of regression 366 | 367 | exercise = np.zeros(len(good_paths), dtype=bool) 368 | exercise[good_paths] = H[good_paths, t] > C 369 | 370 | V[exercise, t] = H[exercise, t] 371 | V[exercise, t + 1 :] = 0 372 | discount_path = V[:, t] == 0 373 | V[discount_path, t] = V[discount_path, t + 1] * df 374 | 375 | V0 = np.mean(V[:, 1]) * df # 376 | return V0 377 | -------------------------------------------------------------------------------- /src/FMNM/CF.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Oct 7 17:57:19 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | 11 | 12 | def cf_normal(u, mu=1, sig=2): 13 | """ 14 | Characteristic function of a Normal random variable 15 | """ 16 | return np.exp(1j * u * mu - 0.5 * u**2 * sig**2) 17 | 18 | 19 | def cf_gamma(u, a=1, b=2): 20 | """ 21 | Characteristic function of a Gamma random variable 22 | - shape: a 23 | - scale: b 24 | """ 25 | return (1 - b * u * 1j) ** (-a) 26 | 27 | 28 | def cf_poisson(u, lam=1): 29 | """ 30 | Characteristic function of a Poisson random variable 31 | - rate: lam 32 | """ 33 | return np.exp(lam * (np.exp(1j * u) - 1)) 34 | 35 | 36 | def cf_mert(u, t=1, mu=1, sig=2, lam=0.8, muJ=0, sigJ=0.5): 37 | """ 38 | Characteristic function of a Merton random variable at time t 39 | mu: drift 40 | sig: diffusion coefficient 41 | lam: jump activity 42 | muJ: jump mean size 43 | sigJ: jump size standard deviation 44 | """ 45 | return np.exp( 46 | t * (1j * u * mu - 0.5 * u**2 * sig**2 + lam * (np.exp(1j * u * muJ - 0.5 * u**2 * sigJ**2) - 1)) 47 | ) 48 | 49 | 50 | def cf_VG(u, t=1, mu=0, theta=-0.1, sigma=0.2, kappa=0.1): 51 | """ 52 | Characteristic function of a Variance Gamma random variable at time t 53 | mu: additional drift 54 | theta: Brownian motion drift 55 | sigma: Brownian motion diffusion 56 | kappa: Gamma process variance 57 | """ 58 | return np.exp(t * (1j * mu * u - np.log(1 - 1j * theta * kappa * u + 0.5 * kappa * sigma**2 * u**2) / kappa)) 59 | 60 | 61 | def cf_NIG(u, t=1, mu=0, theta=-0.1, sigma=0.2, kappa=0.1): 62 | """ 63 | Characteristic function of a Normal Inverse Gaussian random variable at time t 64 | mu: additional drift 65 | theta: Brownian motion drift 66 | sigma: Brownian motion diffusion 67 | kappa: Inverse Gaussian process variance 68 | """ 69 | return np.exp( 70 | t * (1j * mu * u + 1 / kappa - np.sqrt(1 - 2j * theta * kappa * u + kappa * sigma**2 * u**2) / kappa) 71 | ) 72 | 73 | 74 | def cf_Heston(u, t, v0, mu, kappa, theta, sigma, rho): 75 | """ 76 | Heston characteristic function as proposed in the original paper of Heston (1993) 77 | """ 78 | xi = kappa - sigma * rho * u * 1j 79 | d = np.sqrt(xi**2 + sigma**2 * (u**2 + 1j * u)) 80 | g1 = (xi + d) / (xi - d) 81 | cf = np.exp( 82 | 1j * u * mu * t 83 | + (kappa * theta) / (sigma**2) * ((xi + d) * t - 2 * np.log((1 - g1 * np.exp(d * t)) / (1 - g1))) 84 | + (v0 / sigma**2) * (xi + d) * (1 - np.exp(d * t)) / (1 - g1 * np.exp(d * t)) 85 | ) 86 | return cf 87 | 88 | 89 | def cf_Heston_good(u, t, v0, mu, kappa, theta, sigma, rho): 90 | """ 91 | Heston characteristic function as proposed by Schoutens (2004) 92 | """ 93 | xi = kappa - sigma * rho * u * 1j 94 | d = np.sqrt(xi**2 + sigma**2 * (u**2 + 1j * u)) 95 | g1 = (xi + d) / (xi - d) 96 | g2 = 1 / g1 97 | cf = np.exp( 98 | 1j * u * mu * t 99 | + (kappa * theta) / (sigma**2) * ((xi - d) * t - 2 * np.log((1 - g2 * np.exp(-d * t)) / (1 - g2))) 100 | + (v0 / sigma**2) * (xi - d) * (1 - np.exp(-d * t)) / (1 - g2 * np.exp(-d * t)) 101 | ) 102 | return cf 103 | -------------------------------------------------------------------------------- /src/FMNM/FFT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon May 18 20:13:17 2020 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | from scipy.fftpack import ifft 11 | from scipy.interpolate import interp1d 12 | from scipy.integrate import quad 13 | from scipy.optimize import fsolve 14 | 15 | 16 | def fft_Lewis(K, S0, r, T, cf, interp="cubic"): 17 | """ 18 | K = vector of strike 19 | S = spot price scalar 20 | cf = characteristic function 21 | interp can be cubic or linear 22 | """ 23 | N = 2**15 # FFT more efficient for N power of 2 24 | B = 500 # integration limit 25 | dx = B / N 26 | x = np.arange(N) * dx # the final value B is excluded 27 | 28 | weight = np.arange(N) # Simpson weights 29 | weight = 3 + (-1) ** (weight + 1) 30 | weight[0] = 1 31 | weight[N - 1] = 1 32 | 33 | dk = 2 * np.pi / B 34 | b = N * dk / 2 35 | ks = -b + dk * np.arange(N) 36 | 37 | integrand = np.exp(-1j * b * np.arange(N) * dx) * cf(x - 0.5j) * 1 / (x**2 + 0.25) * weight * dx / 3 38 | integral_value = np.real(ifft(integrand) * N) 39 | 40 | if interp == "linear": 41 | spline_lin = interp1d(ks, integral_value, kind="linear") 42 | prices = S0 - np.sqrt(S0 * K) * np.exp(-r * T) / np.pi * spline_lin(np.log(S0 / K)) 43 | elif interp == "cubic": 44 | spline_cub = interp1d(ks, integral_value, kind="cubic") 45 | prices = S0 - np.sqrt(S0 * K) * np.exp(-r * T) / np.pi * spline_cub(np.log(S0 / K)) 46 | return prices 47 | 48 | 49 | def IV_from_Lewis(K, S0, T, r, cf, disp=False): 50 | """Implied Volatility from the Lewis formula 51 | K = strike; S0 = spot stock; T = time to maturity; r = interest rate 52 | cf = characteristic function""" 53 | k = np.log(S0 / K) 54 | 55 | def obj_fun(sig): 56 | integrand = ( 57 | lambda u: np.real( 58 | np.exp(u * k * 1j) 59 | * (cf(u - 0.5j) - np.exp(1j * u * r * T + 0.5 * r * T) * np.exp(-0.5 * T * (u**2 + 0.25) * sig**2)) 60 | ) 61 | * 1 62 | / (u**2 + 0.25) 63 | ) 64 | int_value = quad(integrand, 1e-15, 2000, limit=2000, full_output=1)[0] 65 | return int_value 66 | 67 | X0 = [0.2, 1, 2, 4, 0.0001] # set of initial guess points 68 | for x0 in X0: 69 | x, _, solved, msg = fsolve( 70 | obj_fun, 71 | [ 72 | x0, 73 | ], 74 | full_output=True, 75 | xtol=1e-4, 76 | ) 77 | if solved == 1: 78 | return x[0] 79 | if disp is True: 80 | print("Strike", K, msg) 81 | return -1 82 | -------------------------------------------------------------------------------- /src/FMNM/Heston_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Apr 19 12:13:10 2020 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | from time import time 10 | import numpy as np 11 | import scipy as scp 12 | import scipy.stats as ss 13 | 14 | from FMNM.CF import cf_Heston_good 15 | from FMNM.cython.heston import Heston_paths 16 | from FMNM.probabilities import Q1, Q2 17 | from functools import partial 18 | from FMNM.FFT import fft_Lewis, IV_from_Lewis 19 | 20 | 21 | class Heston_pricer: 22 | """ 23 | Class to price the options with the Heston model by: 24 | - Fourier-inversion. 25 | - Monte Carlo. 26 | """ 27 | 28 | def __init__(self, Option_info, Process_info): 29 | """ 30 | Process_info: of type VG_process. It contains the interest rate r 31 | and the VG parameters (sigma, theta, kappa) 32 | 33 | Option_info: of type Option_param. It contains (S0,K,T) i.e. current price, 34 | strike, maturity in years 35 | """ 36 | self.r = Process_info.mu # interest rate 37 | self.sigma = Process_info.sigma # Heston parameter 38 | self.theta = Process_info.theta # Heston parameter 39 | self.kappa = Process_info.kappa # Heston parameter 40 | self.rho = Process_info.rho # Heston parameter 41 | 42 | self.S0 = Option_info.S0 # current price 43 | self.v0 = Option_info.v0 # spot variance 44 | self.K = Option_info.K # strike 45 | self.T = Option_info.T # maturity in years 46 | 47 | self.exercise = Option_info.exercise 48 | self.payoff = Option_info.payoff 49 | 50 | def payoff_f(self, S): 51 | if self.payoff == "call": 52 | Payoff = np.maximum(S - self.K, 0) 53 | elif self.payoff == "put": 54 | Payoff = np.maximum(self.K - S, 0) 55 | return Payoff 56 | 57 | def MC(self, N, paths, Err=False, Time=False): 58 | """ 59 | Heston Monte Carlo 60 | N = time steps 61 | paths = number of simulated paths 62 | Err = return Standard Error if True 63 | Time = return execution time if True 64 | """ 65 | t_init = time() 66 | 67 | S_T, _ = Heston_paths( 68 | N=N, 69 | paths=paths, 70 | T=self.T, 71 | S0=self.S0, 72 | v0=self.v0, 73 | mu=self.r, 74 | rho=self.rho, 75 | kappa=self.kappa, 76 | theta=self.theta, 77 | sigma=self.sigma, 78 | ) 79 | S_T = S_T.reshape((paths, 1)) 80 | DiscountedPayoff = np.exp(-self.r * self.T) * self.payoff_f(S_T) 81 | V = scp.mean(DiscountedPayoff, axis=0) 82 | std_err = ss.sem(DiscountedPayoff) 83 | 84 | if Err is True: 85 | if Time is True: 86 | elapsed = time() - t_init 87 | return V, std_err, elapsed 88 | else: 89 | return V, std_err 90 | else: 91 | if Time is True: 92 | elapsed = time() - t_init 93 | return V, elapsed 94 | else: 95 | return V 96 | 97 | def Fourier_inversion(self): 98 | """ 99 | Price obtained by inversion of the characteristic function 100 | """ 101 | k = np.log(self.K / self.S0) # log moneyness 102 | cf_H_b_good = partial( 103 | cf_Heston_good, 104 | t=self.T, 105 | v0=self.v0, 106 | mu=self.r, 107 | theta=self.theta, 108 | sigma=self.sigma, 109 | kappa=self.kappa, 110 | rho=self.rho, 111 | ) 112 | 113 | limit_max = 2000 # right limit in the integration 114 | 115 | if self.payoff == "call": 116 | call = self.S0 * Q1(k, cf_H_b_good, limit_max) - self.K * np.exp(-self.r * self.T) * Q2( 117 | k, cf_H_b_good, limit_max 118 | ) 119 | return call 120 | elif self.payoff == "put": 121 | put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_H_b_good, limit_max)) - self.S0 * ( 122 | 1 - Q1(k, cf_H_b_good, limit_max) 123 | ) 124 | return put 125 | else: 126 | raise ValueError("invalid type. Set 'call' or 'put'") 127 | 128 | def FFT(self, K): 129 | """ 130 | FFT method. It returns a vector of prices. 131 | K is an array of strikes 132 | """ 133 | K = np.array(K) 134 | cf_H_b_good = partial( 135 | cf_Heston_good, 136 | t=self.T, 137 | v0=self.v0, 138 | mu=self.r, 139 | theta=self.theta, 140 | sigma=self.sigma, 141 | kappa=self.kappa, 142 | rho=self.rho, 143 | ) 144 | 145 | if self.payoff == "call": 146 | return fft_Lewis(K, self.S0, self.r, self.T, cf_H_b_good, interp="cubic") 147 | elif self.payoff == "put": # put-call parity 148 | return ( 149 | fft_Lewis(K, self.S0, self.r, self.T, cf_H_b_good, interp="cubic") 150 | - self.S0 151 | + K * np.exp(-self.r * self.T) 152 | ) 153 | else: 154 | raise ValueError("invalid type. Set 'call' or 'put'") 155 | 156 | def IV_Lewis(self): 157 | """Implied Volatility from the Lewis formula""" 158 | 159 | cf_H_b_good = partial( 160 | cf_Heston_good, 161 | t=self.T, 162 | v0=self.v0, 163 | mu=self.r, 164 | theta=self.theta, 165 | sigma=self.sigma, 166 | kappa=self.kappa, 167 | rho=self.rho, 168 | ) 169 | if self.payoff == "call": 170 | return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_H_b_good) 171 | elif self.payoff == "put": 172 | raise NotImplementedError 173 | else: 174 | raise ValueError("invalid type. Set 'call' or 'put'") 175 | -------------------------------------------------------------------------------- /src/FMNM/Kalman_filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Tue Nov 5 10:43:12 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | from scipy.optimize import minimize 11 | import scipy.stats as ss 12 | import matplotlib.pyplot as plt 13 | 14 | 15 | class Kalman_regression: 16 | """Kalman Filter algorithm for the linear regression beta estimation. 17 | Alpha is assumed constant. 18 | 19 | INPUT: 20 | X = predictor variable. ndarray, Series or DataFrame. 21 | Y = response variable. 22 | alpha0 = constant alpha. The regression intercept. 23 | beta0 = initial beta. 24 | var_eta = variance of process error 25 | var_eps = variance of measurement error 26 | P0 = initial covariance of beta 27 | """ 28 | 29 | def __init__(self, X, Y, alpha0=None, beta0=None, var_eta=None, var_eps=None, P0=10): 30 | self.alpha0 = alpha0 31 | self.beta0 = beta0 32 | self.var_eta = var_eta 33 | self.var_eps = var_eps 34 | self.P0 = P0 35 | self.X = np.asarray(X) 36 | self.Y = np.asarray(Y) 37 | self.loglikelihood = None 38 | self.R2_pre_fit = None 39 | self.R2_post_fit = None 40 | 41 | self.betas = None 42 | self.Ps = None 43 | 44 | if (self.alpha0 is None) or (self.beta0 is None) or (self.var_eps is None): 45 | self.alpha0, self.beta0, self.var_eps = self.get_OLS_params() 46 | print("alpha0, beta0 and var_eps initialized by OLS") 47 | 48 | #################### enforce X and Y to be numpy arrays ###################### 49 | # @property 50 | # def X(self): 51 | # return self._X 52 | # @X.setter 53 | # def X(self, value): 54 | # if not isinstance(value, np.ndarray): 55 | # raise TypeError('X must be a numpy array') 56 | # self._X = value 57 | # 58 | # @property 59 | # def Y(self): 60 | # return self._Y 61 | # @Y.setter 62 | # def Y(self, value): 63 | # if not isinstance(value, np.ndarray): 64 | # raise TypeError('Y must be a numpy array') 65 | # self._Y = value 66 | ############################################################################### 67 | 68 | def get_OLS_params(self): 69 | """Returns the OLS alpha, beta and sigma^2 (variance of epsilon) 70 | Y = alpha + beta * X + epsilon 71 | """ 72 | beta, alpha, _, _, _ = ss.linregress(self.X, self.Y) 73 | resid = self.Y - beta * self.X - alpha 74 | sig2 = resid.var(ddof=2) 75 | return alpha, beta, sig2 76 | 77 | def set_OLS_params(self): 78 | self.alpha0, self.beta0, self.var_eps = self.get_OLS_params() 79 | 80 | def run(self, X=None, Y=None, var_eta=None, var_eps=None): 81 | """ 82 | Run the Kalman Filter 83 | """ 84 | 85 | if (X is None) and (Y is None): 86 | X = self.X 87 | Y = self.Y 88 | 89 | X = np.asarray(X) 90 | Y = np.asarray(Y) 91 | 92 | N = len(X) 93 | if len(Y) != N: 94 | raise ValueError("Y and X must have same length") 95 | 96 | if var_eta is not None: 97 | self.var_eta = var_eta 98 | if var_eps is not None: 99 | self.var_eps = var_eps 100 | if self.var_eta is None: 101 | raise ValueError("var_eta is None") 102 | 103 | betas = np.zeros_like(X) 104 | Ps = np.zeros_like(X) 105 | res_pre = np.zeros_like(X) # pre-fit residuals 106 | 107 | Y = Y - self.alpha0 # re-define Y 108 | P = self.P0 109 | beta = self.beta0 110 | 111 | log_2pi = np.log(2 * np.pi) 112 | loglikelihood = 0 113 | 114 | for k in range(N): 115 | # Prediction 116 | beta_p = beta # predicted beta 117 | P_p = P + self.var_eta # predicted P 118 | 119 | # ausiliary variables 120 | r = Y[k] - beta_p * X[k] 121 | S = P_p * X[k] ** 2 + self.var_eps 122 | KG = X[k] * P_p / S # Kalman gain 123 | 124 | # Update 125 | beta = beta_p + KG * r 126 | P = P_p * (1 - KG * X[k]) 127 | 128 | loglikelihood += 0.5 * (-log_2pi - np.log(S) - (r**2 / S)) 129 | 130 | betas[k] = beta 131 | Ps[k] = P 132 | res_pre[k] = r 133 | 134 | res_post = Y - X * betas # post fit residuals 135 | sqr_err = Y - np.mean(Y) 136 | R2_pre = 1 - (res_pre @ res_pre) / (sqr_err @ sqr_err) 137 | R2_post = 1 - (res_post @ res_post) / (sqr_err @ sqr_err) 138 | 139 | self.loglikelihood = loglikelihood 140 | self.R2_post_fit = R2_post 141 | self.R2_pre_fit = R2_pre 142 | 143 | self.betas = betas 144 | self.Ps = Ps 145 | 146 | def calibrate_MLE(self): 147 | """Returns the result of the MLE calibration for the Beta Kalman filter, 148 | using the L-BFGS-B method. 149 | The calibrated parameters are var_eta and var_eps. 150 | X, Y = Series, array, or DataFrame for the regression 151 | alpha_tr = initial alpha 152 | beta_tr = initial beta 153 | var_eps_ols = initial guess for the errors 154 | """ 155 | 156 | def minus_likelihood(c): 157 | """Function to minimize in order to calibrate the kalman parameters: 158 | var_eta and var_eps.""" 159 | self.var_eps = c[0] 160 | self.var_eta = c[1] 161 | self.run() 162 | return -1 * self.loglikelihood 163 | 164 | result = minimize( 165 | minus_likelihood, 166 | x0=[self.var_eps, self.var_eps], 167 | method="L-BFGS-B", 168 | bounds=[[1e-15, None], [1e-15, None]], 169 | tol=1e-6, 170 | ) 171 | 172 | if result.success is True: 173 | self.beta0 = self.betas[-1] 174 | self.P0 = self.Ps[-1] 175 | self.var_eps = result.x[0] 176 | self.var_eta = result.x[1] 177 | print("Optimization converged successfully") 178 | print("var_eps = {}, var_eta = {}".format(result.x[0], result.x[1])) 179 | 180 | def calibrate_R2(self, mode="pre-fit"): 181 | """Returns the result of the R2 calibration for the Beta Kalman filter, 182 | using the L-BFGS-B method. 183 | The calibrated parameters is var_eta 184 | """ 185 | 186 | def minus_R2(c): 187 | """Function to minimize in order to calibrate the kalman parameters: 188 | var_eta and var_eps.""" 189 | self.var_eta = c 190 | self.run() 191 | if mode == "pre-fit": 192 | return -1 * self.R2_pre_fit 193 | elif mode == "post-fit": 194 | return -1 * self.R2_post_fit 195 | 196 | result = minimize( 197 | minus_R2, 198 | x0=[self.var_eps], 199 | method="L-BFGS-B", 200 | bounds=[[1e-15, 1]], 201 | tol=1e-6, 202 | ) 203 | 204 | if result.success is True: 205 | self.beta0 = self.betas[-1] 206 | self.P0 = self.Ps[-1] 207 | self.var_eta = result.x[0] 208 | print("Optimization converged successfully") 209 | print("var_eta = {}".format(result.x[0])) 210 | 211 | def RTS_smoother(self, X, Y): 212 | """ 213 | Kalman smoother for the beta estimation. 214 | It uses the Rauch-Tung-Striebel (RTS) algorithm. 215 | """ 216 | self.run(X, Y) 217 | betas, Ps = self.betas, self.Ps 218 | 219 | betas_smooth = np.zeros_like(betas) 220 | Ps_smooth = np.zeros_like(Ps) 221 | betas_smooth[-1] = betas[-1] 222 | Ps_smooth[-1] = Ps[-1] 223 | 224 | for k in range(len(X) - 2, -1, -1): 225 | C = Ps[k] / (Ps[k] + self.var_eta) 226 | betas_smooth[k] = betas[k] + C * (betas_smooth[k + 1] - betas[k]) 227 | Ps_smooth[k] = Ps[k] + C**2 * (Ps_smooth[k + 1] - (Ps[k] + self.var_eta)) 228 | 229 | return betas_smooth, Ps_smooth 230 | 231 | 232 | def rolling_regression_test(X, Y, rolling_window, training_size): 233 | """Rolling regression in the test set""" 234 | 235 | rolling_beta = [] 236 | for i in range(len(X) - training_size): 237 | beta_temp, _, _, _, _ = ss.linregress( 238 | X[1 + i + training_size - rolling_window : 1 + i + training_size], 239 | Y[1 + i + training_size - rolling_window : 1 + i + training_size], 240 | ) 241 | rolling_beta.append(beta_temp) 242 | return rolling_beta 243 | 244 | 245 | def plot_betas(X, Y, true_rho, rho_err, var_eta=None, training_size=250, rolling_window=50): 246 | """ 247 | This function performs all the calculations necessary for the plot of: 248 | - Kalman beta 249 | - Rolling beta 250 | - Smoothed beta 251 | Input: 252 | X, Y: predictor and response variables 253 | true_rho: (an array) the true value of the autocorrelation coefficient 254 | rho_err: (an array) rho with model error 255 | var_eta: If None, MLE estimator is used 256 | training_size: size of the training set 257 | rolling window: for the computation of the rolling regression 258 | """ 259 | 260 | X_train = X[:training_size] 261 | X_test = X[training_size:] 262 | Y_train = Y[:training_size] 263 | Y_test = Y[training_size:] 264 | # beta_tr, alpha_tr, _ ,_ ,_ = ss.linregress(X_train, Y_train) 265 | # resid_tr = Y_train - beta_tr * X_train - alpha_tr 266 | # var_eps = resid_tr.var(ddof=2) 267 | 268 | KR = Kalman_regression(X_train, Y_train) 269 | var_eps = KR.var_eps 270 | 271 | if var_eta is None: 272 | KR.calibrate_MLE() 273 | var_eta, var_eps = KR.var_eta, KR.var_eps 274 | if var_eta < 1e-8: 275 | print(" MLE FAILED. var_eta set equal to var_eps") 276 | var_eta = var_eps 277 | else: 278 | print("MLE parameters") 279 | 280 | print("var_eta = ", var_eta) 281 | print("var_eps = ", var_eps) 282 | 283 | KR.run(X_train, Y_train, var_eps=var_eps, var_eta=var_eta) 284 | KR.beta0, KR.P0 = KR.betas[-1], KR.Ps[-1] 285 | KR.run(X_test, Y_test) 286 | # Kalman 287 | betas_KF, Ps_KF = KR.betas, KR.Ps 288 | # Rolling betas 289 | rolling_beta = rolling_regression_test(X, Y, rolling_window, training_size) 290 | # Smoother 291 | betas_smooth, Ps_smooth = KR.RTS_smoother(X_test, Y_test) 292 | 293 | plt.figure(figsize=(16, 6)) 294 | plt.plot(betas_KF, color="royalblue", label="Kalman filter betas") 295 | plt.plot( 296 | rolling_beta, 297 | color="orange", 298 | label="Rolling beta, window={}".format(rolling_window), 299 | ) 300 | plt.plot(betas_smooth, label="RTS smoother", color="maroon") 301 | plt.plot( 302 | rho_err[training_size + 1 :], 303 | color="springgreen", 304 | marker="o", 305 | linestyle="None", 306 | label="rho with model error", 307 | ) 308 | plt.plot(true_rho[training_size + 1 :], color="black", alpha=1, label="True rho") 309 | plt.fill_between( 310 | x=range(len(betas_KF)), 311 | y1=betas_KF + np.sqrt(Ps_KF), 312 | y2=betas_KF - np.sqrt(Ps_KF), 313 | alpha=0.5, 314 | linewidth=2, 315 | color="seagreen", 316 | label="Kalman Std Dev: $\pm 1 \sigma$", 317 | ) 318 | plt.legend() 319 | plt.title("Kalman results") 320 | 321 | print( 322 | "MSE Rolling regression: ", 323 | np.mean((np.array(rolling_beta) - true_rho[training_size + 1 :]) ** 2), 324 | ) 325 | print("MSE Kalman Filter: ", np.mean((betas_KF - true_rho[training_size + 1 :]) ** 2)) 326 | print( 327 | "MSE RTS Smoother: ", 328 | np.mean((betas_smooth - true_rho[training_size + 1 :]) ** 2), 329 | ) 330 | -------------------------------------------------------------------------------- /src/FMNM/Merton_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Aug 11 09:47:49 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | from scipy import sparse 10 | from scipy.sparse.linalg import splu 11 | from time import time 12 | import numpy as np 13 | import scipy as scp 14 | import scipy.stats as ss 15 | from scipy import signal 16 | import matplotlib.pyplot as plt 17 | from matplotlib import cm 18 | from FMNM.BS_pricer import BS_pricer 19 | from math import factorial 20 | from FMNM.CF import cf_mert 21 | from FMNM.probabilities import Q1, Q2 22 | from functools import partial 23 | from FMNM.FFT import fft_Lewis, IV_from_Lewis 24 | 25 | 26 | class Merton_pricer: 27 | """ 28 | Closed Formula. 29 | Monte Carlo. 30 | Finite-difference PIDE: Explicit-implicit scheme 31 | 32 | 0 = dV/dt + (r -(1/2)sig^2 -m) dV/dx + (1/2)sig^2 d^V/dx^2 33 | + \int[ V(x+y) nu(dy) ] -(r+lam)V 34 | """ 35 | 36 | def __init__(self, Option_info, Process_info): 37 | """ 38 | Process_info: of type Merton_process. It contains (r, sig, lam, muJ, sigJ) i.e. 39 | interest rate, diffusion coefficient, jump activity and jump distribution params 40 | 41 | Option_info: of type Option_param. It contains (S0,K,T) i.e. current price, 42 | strike, maturity in years 43 | """ 44 | self.r = Process_info.r # interest rate 45 | self.sig = Process_info.sig # diffusion coefficient 46 | self.lam = Process_info.lam # jump activity 47 | self.muJ = Process_info.muJ # jump mean 48 | self.sigJ = Process_info.sigJ # jump std 49 | self.exp_RV = Process_info.exp_RV # function to generate exponential Merton Random Variables 50 | 51 | self.S0 = Option_info.S0 # current price 52 | self.K = Option_info.K # strike 53 | self.T = Option_info.T # maturity in years 54 | 55 | self.price = 0 56 | self.S_vec = None 57 | self.price_vec = None 58 | self.mesh = None 59 | self.exercise = Option_info.exercise 60 | self.payoff = Option_info.payoff 61 | 62 | def payoff_f(self, S): 63 | if self.payoff == "call": 64 | Payoff = np.maximum(S - self.K, 0) 65 | elif self.payoff == "put": 66 | Payoff = np.maximum(self.K - S, 0) 67 | return Payoff 68 | 69 | def closed_formula(self): 70 | """ 71 | Merton closed formula. 72 | """ 73 | 74 | m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m 75 | lam2 = self.lam * np.exp(self.muJ + (self.sigJ**2) / 2) 76 | 77 | tot = 0 78 | for i in range(18): 79 | tot += (np.exp(-lam2 * self.T) * (lam2 * self.T) ** i / factorial(i)) * BS_pricer.BlackScholes( 80 | self.payoff, 81 | self.S0, 82 | self.K, 83 | self.T, 84 | self.r - m + i * (self.muJ + 0.5 * self.sigJ**2) / self.T, 85 | np.sqrt(self.sig**2 + (i * self.sigJ**2) / self.T), 86 | ) 87 | return tot 88 | 89 | def Fourier_inversion(self): 90 | """ 91 | Price obtained by inversion of the characteristic function 92 | """ 93 | k = np.log(self.K / self.S0) # log moneyness 94 | m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m 95 | cf_Mert = partial( 96 | cf_mert, 97 | t=self.T, 98 | mu=(self.r - 0.5 * self.sig**2 - m), 99 | sig=self.sig, 100 | lam=self.lam, 101 | muJ=self.muJ, 102 | sigJ=self.sigJ, 103 | ) 104 | 105 | if self.payoff == "call": 106 | call = self.S0 * Q1(k, cf_Mert, np.inf) - self.K * np.exp(-self.r * self.T) * Q2( 107 | k, cf_Mert, np.inf 108 | ) # pricing function 109 | return call 110 | elif self.payoff == "put": 111 | put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_Mert, np.inf)) - self.S0 * ( 112 | 1 - Q1(k, cf_Mert, np.inf) 113 | ) # pricing function 114 | return put 115 | else: 116 | raise ValueError("invalid type. Set 'call' or 'put'") 117 | 118 | def FFT(self, K): 119 | """ 120 | FFT method. It returns a vector of prices. 121 | K is an array of strikes 122 | """ 123 | K = np.array(K) 124 | m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m 125 | cf_Mert = partial( 126 | cf_mert, 127 | t=self.T, 128 | mu=(self.r - 0.5 * self.sig**2 - m), 129 | sig=self.sig, 130 | lam=self.lam, 131 | muJ=self.muJ, 132 | sigJ=self.sigJ, 133 | ) 134 | 135 | if self.payoff == "call": 136 | return fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic") 137 | elif self.payoff == "put": # put-call parity 138 | return ( 139 | fft_Lewis(K, self.S0, self.r, self.T, cf_Mert, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T) 140 | ) 141 | else: 142 | raise ValueError("invalid type. Set 'call' or 'put'") 143 | 144 | def IV_Lewis(self): 145 | """Implied Volatility from the Lewis formula""" 146 | 147 | m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m 148 | cf_Mert = partial( 149 | cf_mert, 150 | t=self.T, 151 | mu=(self.r - 0.5 * self.sig**2 - m), 152 | sig=self.sig, 153 | lam=self.lam, 154 | muJ=self.muJ, 155 | sigJ=self.sigJ, 156 | ) 157 | 158 | if self.payoff == "call": 159 | return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_Mert) 160 | elif self.payoff == "put": 161 | raise NotImplementedError 162 | else: 163 | raise ValueError("invalid type. Set 'call' or 'put'") 164 | 165 | def MC(self, N, Err=False, Time=False): 166 | """ 167 | Merton Monte Carlo 168 | Err = return Standard Error if True 169 | Time = return execution time if True 170 | """ 171 | t_init = time() 172 | 173 | S_T = self.exp_RV(self.S0, self.T, N) 174 | V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0) 175 | 176 | if Err is True: 177 | if Time is True: 178 | elapsed = time() - t_init 179 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed 180 | else: 181 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)) 182 | else: 183 | if Time is True: 184 | elapsed = time() - t_init 185 | return V, elapsed 186 | else: 187 | return V 188 | 189 | def PIDE_price(self, steps, Time=False): 190 | """ 191 | steps = tuple with number of space steps and time steps 192 | payoff = "call" or "put" 193 | exercise = "European" or "American" 194 | Time = Boolean. Execution time. 195 | """ 196 | t_init = time() 197 | 198 | Nspace = steps[0] 199 | Ntime = steps[1] 200 | 201 | S_max = 6 * float(self.K) 202 | S_min = float(self.K) / 6 203 | x_max = np.log(S_max) 204 | x_min = np.log(S_min) 205 | 206 | dev_X = np.sqrt(self.lam * self.sigJ**2 + self.lam * self.muJ**2) 207 | 208 | dx = (x_max - x_min) / (Nspace - 1) 209 | extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C. 210 | x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization 211 | t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization 212 | 213 | Payoff = self.payoff_f(np.exp(x)) 214 | offset = np.zeros(Nspace - 2) 215 | V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization 216 | 217 | if self.payoff == "call": 218 | V[:, -1] = Payoff # terminal conditions 219 | V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones( 220 | (extraP + 1, Ntime) 221 | ) - self.K * np.exp(-self.r * t[::-1]) * np.ones( 222 | (extraP + 1, Ntime) 223 | ) # boundary condition 224 | V[: extraP + 1, :] = 0 225 | else: 226 | V[:, -1] = Payoff 227 | V[-extraP - 1 :, :] = 0 228 | V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime)) 229 | 230 | cdf = ss.norm.cdf( 231 | [np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2))], 232 | loc=self.muJ, 233 | scale=self.sigJ, 234 | )[0] 235 | nu = self.lam * (cdf[1:] - cdf[:-1]) 236 | 237 | lam_appr = sum(nu) 238 | m_appr = np.array([np.exp(i * dx) - 1 for i in range(-(extraP + 1), extraP + 2)]) @ nu 239 | 240 | sig2 = self.sig**2 241 | dxx = dx**2 242 | a = (dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx - sig2 / dxx) 243 | b = 1 + dt * (sig2 / dxx + self.r + lam_appr) 244 | c = -(dt / 2) * ((self.r - m_appr - 0.5 * sig2) / dx + sig2 / dxx) 245 | 246 | D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc() 247 | DD = splu(D) 248 | if self.exercise == "European": 249 | for i in range(Ntime - 2, -1, -1): 250 | offset[0] = a * V[extraP, i] 251 | offset[-1] = c * V[-1 - extraP, i] 252 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 253 | V[:, i + 1], nu[::-1], mode="valid", method="fft" 254 | ) 255 | V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset) 256 | elif self.exercise == "American": 257 | for i in range(Ntime - 2, -1, -1): 258 | offset[0] = a * V[extraP, i] 259 | offset[-1] = c * V[-1 - extraP, i] 260 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 261 | V[:, i + 1], nu[::-1], mode="valid", method="fft" 262 | ) 263 | V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1]) 264 | 265 | X0 = np.log(self.S0) # current log-price 266 | self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S 267 | self.price = np.interp(X0, x, V[:, 0]) 268 | self.price_vec = V[extraP + 1 : -extraP - 1, 0] 269 | self.mesh = V[extraP + 1 : -extraP - 1, :] 270 | 271 | if Time is True: 272 | elapsed = time() - t_init 273 | return self.price, elapsed 274 | else: 275 | return self.price 276 | 277 | def plot(self, axis=None): 278 | if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray: 279 | self.PIDE_price((5000, 4000)) 280 | 281 | plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff") 282 | plt.plot(self.S_vec, self.price_vec, color="red", label="Merton curve") 283 | if type(axis) == list: 284 | plt.axis(axis) 285 | plt.xlabel("S") 286 | plt.ylabel("price") 287 | plt.title("Merton price") 288 | plt.legend(loc="upper left") 289 | plt.show() 290 | 291 | def mesh_plt(self): 292 | if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray: 293 | self.PDE_price((7000, 5000)) 294 | 295 | fig = plt.figure() 296 | ax = fig.add_subplot(111, projection="3d") 297 | 298 | X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec) 299 | ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean) 300 | ax.set_title("Merton price surface") 301 | ax.set_xlabel("S") 302 | ax.set_ylabel("t") 303 | ax.set_zlabel("V") 304 | ax.view_init(30, -100) # this function rotates the 3d plot 305 | plt.show() 306 | -------------------------------------------------------------------------------- /src/FMNM/NIG_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Fri Nov 1 12:47:00 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | from scipy import sparse 10 | from scipy.sparse.linalg import splu 11 | from time import time 12 | import numpy as np 13 | import scipy as scp 14 | from scipy import signal 15 | from scipy.integrate import quad 16 | import scipy.stats as ss 17 | import scipy.special as scps 18 | 19 | import matplotlib.pyplot as plt 20 | from matplotlib import cm 21 | from FMNM.CF import cf_NIG 22 | from FMNM.probabilities import Q1, Q2 23 | from functools import partial 24 | 25 | 26 | class NIG_pricer: 27 | """ 28 | Closed Formula. 29 | Monte Carlo. 30 | Finite-difference PIDE: Explicit-implicit scheme, with Brownian approximation 31 | 32 | 0 = dV/dt + (r -(1/2)sig^2 -w) dV/dx + (1/2)sig^2 d^V/dx^2 33 | + \int[ V(x+y) nu(dy) ] -(r+lam)V 34 | """ 35 | 36 | def __init__(self, Option_info, Process_info): 37 | """ 38 | Process_info: of type NIG_process. It contains the interest rate r 39 | and the NIG parameters (sigma, theta, kappa) 40 | 41 | Option_info: of type Option_param. 42 | It contains (S0,K,T) i.e. current price, strike, maturity in years 43 | """ 44 | self.r = Process_info.r # interest rate 45 | self.sigma = Process_info.sigma # NIG parameter 46 | self.theta = Process_info.theta # NIG parameter 47 | self.kappa = Process_info.kappa # NIG parameter 48 | self.exp_RV = Process_info.exp_RV # function to generate exponential NIG Random Variables 49 | 50 | self.S0 = Option_info.S0 # current price 51 | self.K = Option_info.K # strike 52 | self.T = Option_info.T # maturity in years 53 | 54 | self.price = 0 55 | self.S_vec = None 56 | self.price_vec = None 57 | self.mesh = None 58 | self.exercise = Option_info.exercise 59 | self.payoff = Option_info.payoff 60 | 61 | def payoff_f(self, S): 62 | if self.payoff == "call": 63 | Payoff = np.maximum(S - self.K, 0) 64 | elif self.payoff == "put": 65 | Payoff = np.maximum(self.K - S, 0) 66 | return Payoff 67 | 68 | def Fourier_inversion(self): 69 | """ 70 | Price obtained by inversion of the characteristic function 71 | """ 72 | k = np.log(self.K / self.S0) # log moneyness 73 | w = ( 74 | 1 - np.sqrt(1 - 2 * self.theta * self.kappa - self.kappa * self.sigma**2) 75 | ) / self.kappa # martingale correction 76 | 77 | cf_NIG_b = partial( 78 | cf_NIG, 79 | t=self.T, 80 | mu=(self.r - w), 81 | theta=self.theta, 82 | sigma=self.sigma, 83 | kappa=self.kappa, 84 | ) 85 | 86 | if self.payoff == "call": 87 | call = self.S0 * Q1(k, cf_NIG_b, np.inf) - self.K * np.exp(-self.r * self.T) * Q2( 88 | k, cf_NIG_b, np.inf 89 | ) # pricing function 90 | return call 91 | elif self.payoff == "put": 92 | put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_NIG_b, np.inf)) - self.S0 * ( 93 | 1 - Q1(k, cf_NIG_b, np.inf) 94 | ) # pricing function 95 | return put 96 | else: 97 | raise ValueError("invalid type. Set 'call' or 'put'") 98 | 99 | def MC(self, N, Err=False, Time=False): 100 | """ 101 | NIG Monte Carlo 102 | Err = return Standard Error if True 103 | Time = return execution time if True 104 | """ 105 | t_init = time() 106 | 107 | S_T = self.exp_RV(self.S0, self.T, N) 108 | V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T)) 109 | 110 | if Err is True: 111 | if Time is True: 112 | elapsed = time() - t_init 113 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed 114 | else: 115 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)) 116 | else: 117 | if Time is True: 118 | elapsed = time() - t_init 119 | return V, elapsed 120 | else: 121 | return V 122 | 123 | def NIG_measure(self, x): 124 | A = self.theta / (self.sigma**2) 125 | B = np.sqrt(self.theta**2 + self.sigma**2 / self.kappa) / self.sigma**2 126 | C = np.sqrt(self.theta**2 + self.sigma**2 / self.kappa) / (np.pi * self.sigma * np.sqrt(self.kappa)) 127 | return C / np.abs(x) * np.exp(A * (x)) * scps.kv(1, B * np.abs(x)) 128 | 129 | def PIDE_price(self, steps, Time=False): 130 | """ 131 | steps = tuple with number of space steps and time steps 132 | payoff = "call" or "put" 133 | exercise = "European" or "American" 134 | Time = Boolean. Execution time. 135 | """ 136 | t_init = time() 137 | 138 | Nspace = steps[0] 139 | Ntime = steps[1] 140 | 141 | S_max = 2000 * float(self.K) 142 | S_min = float(self.K) / 2000 143 | x_max = np.log(S_max) 144 | x_min = np.log(S_min) 145 | 146 | dev_X = np.sqrt(self.sigma**2 + self.theta**2 * self.kappa) # std dev NIG process 147 | 148 | dx = (x_max - x_min) / (Nspace - 1) 149 | extraP = int(np.floor(7 * dev_X / dx)) # extra points beyond the B.C. 150 | x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization 151 | t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization 152 | 153 | Payoff = self.payoff_f(np.exp(x)) 154 | offset = np.zeros(Nspace - 2) 155 | V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization 156 | 157 | if self.payoff == "call": 158 | V[:, -1] = Payoff # terminal conditions 159 | V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones( 160 | (extraP + 1, Ntime) 161 | ) - self.K * np.exp(-self.r * t[::-1]) * np.ones( 162 | (extraP + 1, Ntime) 163 | ) # boundary condition 164 | V[: extraP + 1, :] = 0 165 | else: 166 | V[:, -1] = Payoff 167 | V[-extraP - 1 :, :] = 0 168 | V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime)) 169 | 170 | eps = 1.5 * dx # the cutoff near 0 171 | lam = ( 172 | quad(self.NIG_measure, -(extraP + 1.5) * dx, -eps)[0] + quad(self.NIG_measure, eps, (extraP + 1.5) * dx)[0] 173 | ) # approximated intensity 174 | 175 | def int_w(y): 176 | return (np.exp(y) - 1) * self.NIG_measure(y) 177 | 178 | def int_s(y): 179 | return y**2 * self.NIG_measure(y) 180 | 181 | w = quad(int_w, -(extraP + 1.5) * dx, -eps)[0] + quad(int_w, eps, (extraP + 1.5) * dx)[0] # is the approx of w 182 | sig2 = quad(int_s, -eps, eps, points=0)[0] # the small jumps variance 183 | 184 | dxx = dx * dx 185 | a = (dt / 2) * ((self.r - w - 0.5 * sig2) / dx - sig2 / dxx) 186 | b = 1 + dt * (sig2 / dxx + self.r + lam) 187 | c = -(dt / 2) * ((self.r - w - 0.5 * sig2) / dx + sig2 / dxx) 188 | D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc() 189 | DD = splu(D) 190 | 191 | nu = np.zeros(2 * extraP + 3) # Lévy measure vector 192 | x_med = extraP + 1 # middle point in nu vector 193 | x_nu = np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2)) # integration domain 194 | for i in range(len(nu)): 195 | if (i == x_med) or (i == x_med - 1) or (i == x_med + 1): 196 | continue 197 | nu[i] = quad(self.NIG_measure, x_nu[i], x_nu[i + 1])[0] 198 | 199 | if self.exercise == "European": 200 | # Backward iteration 201 | for i in range(Ntime - 2, -1, -1): 202 | offset[0] = a * V[extraP, i] 203 | offset[-1] = c * V[-1 - extraP, i] 204 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 205 | V[:, i + 1], nu[::-1], mode="valid", method="auto" 206 | ) 207 | V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset) 208 | elif self.exercise == "American": 209 | for i in range(Ntime - 2, -1, -1): 210 | offset[0] = a * V[extraP, i] 211 | offset[-1] = c * V[-1 - extraP, i] 212 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 213 | V[:, i + 1], nu[::-1], mode="valid", method="auto" 214 | ) 215 | V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1]) 216 | 217 | X0 = np.log(self.S0) # current log-price 218 | self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S 219 | self.price = np.interp(X0, x, V[:, 0]) 220 | self.price_vec = V[extraP + 1 : -extraP - 1, 0] 221 | self.mesh = V[extraP + 1 : -extraP - 1, :] 222 | 223 | if Time is True: 224 | elapsed = time() - t_init 225 | return self.price, elapsed 226 | else: 227 | return self.price 228 | 229 | def plot(self, axis=None): 230 | if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray: 231 | self.PIDE_price((5000, 4000)) 232 | 233 | plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff") 234 | plt.plot(self.S_vec, self.price_vec, color="red", label="NIG curve") 235 | if type(axis) == list: 236 | plt.axis(axis) 237 | plt.xlabel("S") 238 | plt.ylabel("price") 239 | plt.title("NIG price") 240 | plt.legend(loc="best") 241 | plt.show() 242 | 243 | def mesh_plt(self): 244 | if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray: 245 | self.PDE_price((7000, 5000)) 246 | 247 | fig = plt.figure() 248 | ax = fig.add_subplot(111, projection="3d") 249 | 250 | X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec) 251 | ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean) 252 | ax.set_title("NIG price surface") 253 | ax.set_xlabel("S") 254 | ax.set_ylabel("t") 255 | ax.set_zlabel("V") 256 | ax.view_init(30, -100) # this function rotates the 3d plot 257 | plt.show() 258 | -------------------------------------------------------------------------------- /src/FMNM/Parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jun 10 09:56:25 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | 10 | class Option_param: 11 | """ 12 | Option class wants the option parameters: 13 | S0 = current stock price 14 | K = Strike price 15 | T = time to maturity 16 | v0 = (optional) spot variance 17 | exercise = European or American 18 | """ 19 | 20 | def __init__(self, S0=15, K=15, T=1, v0=0.04, payoff="call", exercise="European"): 21 | self.S0 = S0 22 | self.v0 = v0 23 | self.K = K 24 | self.T = T 25 | 26 | if exercise == "European" or exercise == "American": 27 | self.exercise = exercise 28 | else: 29 | raise ValueError("invalid type. Set 'European' or 'American'") 30 | 31 | if payoff == "call" or payoff == "put": 32 | self.payoff = payoff 33 | else: 34 | raise ValueError("invalid type. Set 'call' or 'put'") 35 | -------------------------------------------------------------------------------- /src/FMNM/Processes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sat Jul 27 17:06:01 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | import scipy.stats as ss 11 | from FMNM.probabilities import VG_pdf 12 | from scipy.optimize import minimize 13 | from statsmodels.tools.numdiff import approx_hess 14 | import pandas as pd 15 | 16 | 17 | class Diffusion_process: 18 | """ 19 | Class for the diffusion process: 20 | r = risk free constant rate 21 | sig = constant diffusion coefficient 22 | mu = constant drift 23 | """ 24 | 25 | def __init__(self, r=0.1, sig=0.2, mu=0.1): 26 | self.r = r 27 | self.mu = mu 28 | if sig <= 0: 29 | raise ValueError("sig must be positive") 30 | else: 31 | self.sig = sig 32 | 33 | def exp_RV(self, S0, T, N): 34 | W = ss.norm.rvs((self.r - 0.5 * self.sig**2) * T, np.sqrt(T) * self.sig, N) 35 | S_T = S0 * np.exp(W) 36 | return S_T.reshape((N, 1)) 37 | 38 | 39 | class Merton_process: 40 | """ 41 | Class for the Merton process: 42 | r = risk free constant rate 43 | sig = constant diffusion coefficient 44 | lam = jump activity 45 | muJ = jump mean 46 | sigJ = jump standard deviation 47 | """ 48 | 49 | def __init__(self, r=0.1, sig=0.2, lam=0.8, muJ=0, sigJ=0.5): 50 | self.r = r 51 | self.lam = lam 52 | self.muJ = muJ 53 | if sig < 0 or sigJ < 0: 54 | raise ValueError("sig and sigJ must be positive") 55 | else: 56 | self.sig = sig 57 | self.sigJ = sigJ 58 | 59 | # moments 60 | self.var = self.sig**2 + self.lam * self.sigJ**2 + self.lam * self.muJ**2 61 | self.skew = self.lam * (3 * self.sigJ**2 * self.muJ + self.muJ**3) / self.var ** (1.5) 62 | self.kurt = self.lam * (3 * self.sigJ**3 + 6 * self.sigJ**2 * self.muJ**2 + self.muJ**4) / self.var**2 63 | 64 | def exp_RV(self, S0, T, N): 65 | m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m 66 | W = ss.norm.rvs(0, 1, N) # The normal RV vector 67 | P = ss.poisson.rvs(self.lam * T, size=N) # Poisson random vector (number of jumps) 68 | Jumps = np.asarray([ss.norm.rvs(self.muJ, self.sigJ, ind).sum() for ind in P]) # Jumps vector 69 | S_T = S0 * np.exp( 70 | (self.r - 0.5 * self.sig**2 - m) * T + np.sqrt(T) * self.sig * W + Jumps 71 | ) # Martingale exponential Merton 72 | return S_T.reshape((N, 1)) 73 | 74 | 75 | class VG_process: 76 | """ 77 | Class for the Variance Gamma process: 78 | r = risk free constant rate 79 | Using the representation of Brownian subordination, the parameters are: 80 | theta = drift of the Brownian motion 81 | sigma = standard deviation of the Brownian motion 82 | kappa = variance of the of the Gamma process 83 | """ 84 | 85 | def __init__(self, r=0.1, sigma=0.2, theta=-0.1, kappa=0.1): 86 | self.r = r 87 | self.c = self.r 88 | self.theta = theta 89 | self.kappa = kappa 90 | if sigma < 0: 91 | raise ValueError("sigma must be positive") 92 | else: 93 | self.sigma = sigma 94 | 95 | # moments 96 | self.mean = self.c + self.theta 97 | self.var = self.sigma**2 + self.theta**2 * self.kappa 98 | self.skew = (2 * self.theta**3 * self.kappa**2 + 3 * self.sigma**2 * self.theta * self.kappa) / ( 99 | self.var ** (1.5) 100 | ) 101 | self.kurt = ( 102 | 3 * self.sigma**4 * self.kappa 103 | + 12 * self.sigma**2 * self.theta**2 * self.kappa**2 104 | + 6 * self.theta**4 * self.kappa**3 105 | ) / (self.var**2) 106 | 107 | def exp_RV(self, S0, T, N): 108 | w = -np.log(1 - self.theta * self.kappa - self.kappa / 2 * self.sigma**2) / self.kappa # coefficient w 109 | rho = 1 / self.kappa 110 | G = ss.gamma(rho * T).rvs(N) / rho # The gamma RV 111 | Norm = ss.norm.rvs(0, 1, N) # The normal RV 112 | VG = self.theta * G + self.sigma * np.sqrt(G) * Norm # VG process at final time G 113 | S_T = S0 * np.exp((self.r - w) * T + VG) # Martingale exponential VG 114 | return S_T.reshape((N, 1)) 115 | 116 | def path(self, T=1, N=10000, paths=1): 117 | """ 118 | Creates Variance Gamma paths 119 | N = number of time points (time steps are N-1) 120 | paths = number of generated paths 121 | """ 122 | dt = T / (N - 1) # time interval 123 | X0 = np.zeros((paths, 1)) 124 | G = ss.gamma(dt / self.kappa, scale=self.kappa).rvs(size=(paths, N - 1)) # The gamma RV 125 | Norm = ss.norm.rvs(loc=0, scale=1, size=(paths, N - 1)) # The normal RV 126 | increments = self.c * dt + self.theta * G + self.sigma * np.sqrt(G) * Norm 127 | X = np.concatenate((X0, increments), axis=1).cumsum(1) 128 | return X 129 | 130 | def fit_from_data(self, data, dt=1, method="Nelder-Mead"): 131 | """ 132 | Fit the 4 parameters of the VG process using MM (method of moments), 133 | Nelder-Mead, L-BFGS-B. 134 | 135 | data (array): datapoints 136 | dt (float): is the increment time 137 | 138 | Returns (c, theta, sigma, kappa) 139 | """ 140 | X = data 141 | sigma_mm = np.std(X) / np.sqrt(dt) 142 | kappa_mm = dt * ss.kurtosis(X) / 3 143 | theta_mm = np.sqrt(dt) * ss.skew(X) * sigma_mm / (3 * kappa_mm) 144 | c_mm = np.mean(X) / dt - theta_mm 145 | 146 | def log_likely(x, data, T): 147 | return (-1) * np.sum(np.log(VG_pdf(data, T, x[0], x[1], x[2], x[3]))) 148 | 149 | if method == "L-BFGS-B": 150 | if theta_mm < 0: 151 | result = minimize( 152 | log_likely, 153 | x0=[c_mm, theta_mm, sigma_mm, kappa_mm], 154 | method="L-BFGS-B", 155 | args=(X, dt), 156 | tol=1e-8, 157 | bounds=[[-0.5, 0.5], [-0.6, -1e-15], [1e-15, 1], [1e-15, 2]], 158 | ) 159 | else: 160 | result = minimize( 161 | log_likely, 162 | x0=[c_mm, theta_mm, sigma_mm, kappa_mm], 163 | method="L-BFGS-B", 164 | args=(X, dt), 165 | tol=1e-8, 166 | bounds=[[-0.5, 0.5], [1e-15, 0.6], [1e-15, 1], [1e-15, 2]], 167 | ) 168 | print(result.message) 169 | elif method == "Nelder-Mead": 170 | result = minimize( 171 | log_likely, 172 | x0=[c_mm, theta_mm, sigma_mm, kappa_mm], 173 | method="Nelder-Mead", 174 | args=(X, dt), 175 | options={"disp": False, "maxfev": 3000}, 176 | tol=1e-8, 177 | ) 178 | print(result.message) 179 | elif "MM": 180 | self.c, self.theta, self.sigma, self.kappa = ( 181 | c_mm, 182 | theta_mm, 183 | sigma_mm, 184 | kappa_mm, 185 | ) 186 | return 187 | self.c, self.theta, self.sigma, self.kappa = result.x 188 | 189 | 190 | class Heston_process: 191 | """ 192 | Class for the Heston process: 193 | r = risk free constant rate 194 | rho = correlation between stock noise and variance noise 195 | theta = long term mean of the variance process 196 | sigma = volatility coefficient of the variance process 197 | kappa = mean reversion coefficient for the variance process 198 | """ 199 | 200 | def __init__(self, mu=0.1, rho=0, sigma=0.2, theta=-0.1, kappa=0.1): 201 | self.mu = mu 202 | if np.abs(rho) > 1: 203 | raise ValueError("|rho| must be <=1") 204 | self.rho = rho 205 | if theta < 0 or sigma < 0 or kappa < 0: 206 | raise ValueError("sigma,theta,kappa must be positive") 207 | else: 208 | self.theta = theta 209 | self.sigma = sigma 210 | self.kappa = kappa 211 | 212 | def path(self, S0, v0, N, T=1): 213 | """ 214 | Produces one path of the Heston process. 215 | N = number of time steps 216 | T = Time in years 217 | Returns two arrays S (price) and v (variance). 218 | """ 219 | 220 | MU = np.array([0, 0]) 221 | COV = np.matrix([[1, self.rho], [self.rho, 1]]) 222 | W = ss.multivariate_normal.rvs(mean=MU, cov=COV, size=N - 1) 223 | W_S = W[:, 0] # Stock Brownian motion: W_1 224 | W_v = W[:, 1] # Variance Brownian motion: W_2 225 | 226 | # Initialize vectors 227 | T_vec, dt = np.linspace(0, T, N, retstep=True) 228 | dt_sq = np.sqrt(dt) 229 | 230 | X0 = np.log(S0) 231 | v = np.zeros(N) 232 | v[0] = v0 233 | X = np.zeros(N) 234 | X[0] = X0 235 | 236 | # Generate paths 237 | for t in range(0, N - 1): 238 | v_sq = np.sqrt(v[t]) 239 | v[t + 1] = np.abs(v[t] + self.kappa * (self.theta - v[t]) * dt + self.sigma * v_sq * dt_sq * W_v[t]) 240 | X[t + 1] = X[t] + (self.mu - 0.5 * v[t]) * dt + v_sq * dt_sq * W_S[t] 241 | 242 | return np.exp(X), v 243 | 244 | 245 | class NIG_process: 246 | """ 247 | Class for the Normal Inverse Gaussian process: 248 | r = risk free constant rate 249 | Using the representation of Brownian subordination, the parameters are: 250 | theta = drift of the Brownian motion 251 | sigma = standard deviation of the Brownian motion 252 | kappa = variance of the of the Gamma process 253 | """ 254 | 255 | def __init__(self, r=0.1, sigma=0.2, theta=-0.1, kappa=0.1): 256 | self.r = r 257 | self.theta = theta 258 | if sigma < 0 or kappa < 0: 259 | raise ValueError("sigma and kappa must be positive") 260 | else: 261 | self.sigma = sigma 262 | self.kappa = kappa 263 | 264 | # moments 265 | self.var = self.sigma**2 + self.theta**2 * self.kappa 266 | self.skew = (3 * self.theta**3 * self.kappa**2 + 3 * self.sigma**2 * self.theta * self.kappa) / ( 267 | self.var ** (1.5) 268 | ) 269 | self.kurt = ( 270 | 3 * self.sigma**4 * self.kappa 271 | + 18 * self.sigma**2 * self.theta**2 * self.kappa**2 272 | + 15 * self.theta**4 * self.kappa**3 273 | ) / (self.var**2) 274 | 275 | def exp_RV(self, S0, T, N): 276 | lam = T**2 / self.kappa # scale for the IG process 277 | mu_s = T / lam # scaled mean 278 | w = (1 - np.sqrt(1 - 2 * self.theta * self.kappa - self.kappa * self.sigma**2)) / self.kappa 279 | IG = ss.invgauss.rvs(mu=mu_s, scale=lam, size=N) # The IG RV 280 | Norm = ss.norm.rvs(0, 1, N) # The normal RV 281 | X = self.theta * IG + self.sigma * np.sqrt(IG) * Norm # NIG random vector 282 | S_T = S0 * np.exp((self.r - w) * T + X) # exponential dynamics 283 | return S_T.reshape((N, 1)) 284 | 285 | 286 | class GARCH: 287 | """ 288 | Class for the GARCH(1,1) process. Variance process: 289 | 290 | V(t) = omega + alpha R^2(t-1) + beta V(t-1) 291 | 292 | VL: Unconditional variance >=0 293 | alpha: coefficient > 0 294 | beta: coefficient > 0 295 | gamma = 1 - alpha - beta 296 | omega = gamma*VL 297 | """ 298 | 299 | def __init__(self, VL=0.04, alpha=0.08, beta=0.9): 300 | if VL < 0 or alpha <= 0 or beta <= 0: 301 | raise ValueError("VL>=0, alpha>0 and beta>0") 302 | else: 303 | self.VL = VL 304 | self.alpha = alpha 305 | self.beta = beta 306 | self.gamma = 1 - self.alpha - self.beta 307 | self.omega = self.gamma * self.VL 308 | 309 | def path(self, N=1000): 310 | """ 311 | Generates a path with N points. 312 | Returns the return process R and the variance process var 313 | """ 314 | eps = ss.norm.rvs(loc=0, scale=1, size=N) 315 | R = np.zeros_like(eps) 316 | var = np.zeros_like(eps) 317 | for i in range(N): 318 | var[i] = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var[i - 1] 319 | R[i] = np.sqrt(var[i]) * eps[i] 320 | return R, var 321 | 322 | def fit_from_data(self, data, disp=True): 323 | """ 324 | MLE estimator for the GARCH 325 | """ 326 | # Automatic re-scaling: 327 | # 1. the solver has problems with positive derivative in linesearch. 328 | # 2. the log has overflows using small values 329 | n = np.floor(np.log10(np.abs(data.mean()))) 330 | R = data / 10**n 331 | 332 | # initial guesses 333 | a0 = 0.05 334 | b0 = 0.9 335 | g0 = 1 - a0 - b0 336 | w0 = g0 * np.var(R) 337 | 338 | # bounds and constraint 339 | bounds = ((0, None), (0, 1), (0, 1)) 340 | 341 | def sum_small_1(x): 342 | return 1 - x[1] - x[2] 343 | 344 | cons = {"fun": sum_small_1, "type": "ineq"} 345 | 346 | def log_likely(x): 347 | var = R[0] ** 2 # initial variance 348 | N = len(R) 349 | log_lik = 0 350 | for i in range(1, N): 351 | var = x[0] + x[1] * R[i - 1] ** 2 + x[2] * var # variance update 352 | log_lik += -np.log(var) - (R[i] ** 2 / var) 353 | return (-1) * log_lik 354 | 355 | result = minimize( 356 | log_likely, 357 | x0=[w0, a0, b0], 358 | method="SLSQP", 359 | bounds=bounds, 360 | constraints=cons, 361 | tol=1e-8, 362 | options={"maxiter": 150}, 363 | ) 364 | print(result.message) 365 | self.omega = result.x[0] * 10 ** (2 * n) 366 | self.alpha, self.beta = result.x[1:] 367 | self.gamma = 1 - self.alpha - self.beta 368 | self.VL = self.omega / self.gamma 369 | 370 | if disp is True: 371 | hess = approx_hess(result.x, log_likely) # hessian by finite differences 372 | se = np.sqrt(np.diag(np.linalg.inv(hess))) # standard error 373 | cv = ss.norm.ppf(1.0 - 0.05 / 2.0) # alpha=0.05 374 | p_val = ss.norm.sf(np.abs(result.x / se)) # survival function 375 | 376 | df = pd.DataFrame(index=["omega", "alpha", "beta"]) 377 | df["Params"] = result.x 378 | df["SE"] = se 379 | df["P-val"] = p_val 380 | df["95% CI lower"] = result.x - cv * se 381 | df["95% CI upper"] = result.x + cv * se 382 | df.loc["omega", ["Params", "SE", "95% CI lower", "95% CI upper"]] *= 10 ** (2 * n) 383 | print(df) 384 | 385 | def log_likelihood(self, R, last_var=True): 386 | """ 387 | Computes the log-likelihood and optionally returns the last value 388 | of the variance 389 | """ 390 | var = R[0] ** 2 # initial variance 391 | N = len(R) 392 | log_lik = 0 393 | log_2pi = np.log(2 * np.pi) 394 | for i in range(1, N): 395 | var = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var # variance update 396 | log_lik += 0.5 * (-log_2pi - np.log(var) - (R[i] ** 2 / var)) 397 | if last_var is True: 398 | return log_lik, var 399 | else: 400 | return log_lik 401 | 402 | def generate_var(self, R, R0, var0): 403 | """ 404 | generate the variance process. 405 | R (array): return array 406 | R0: initial value of the returns 407 | var0: initial value of the variance 408 | """ 409 | N = len(R) 410 | var = np.zeros(N) 411 | var[0] = self.omega + self.alpha * (R0**2) + self.beta * var0 412 | for i in range(1, N): 413 | var[i] = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var[i - 1] 414 | return var 415 | 416 | 417 | class OU_process: 418 | """ 419 | Class for the OU process: 420 | theta = long term mean 421 | sigma = diffusion coefficient 422 | kappa = mean reversion coefficient 423 | """ 424 | 425 | def __init__(self, sigma=0.2, theta=-0.1, kappa=0.1): 426 | self.theta = theta 427 | if sigma < 0 or kappa < 0: 428 | raise ValueError("sigma,theta,kappa must be positive") 429 | else: 430 | self.sigma = sigma 431 | self.kappa = kappa 432 | 433 | def path(self, X0=0, T=1, N=10000, paths=1): 434 | """ 435 | Produces a matrix of OU process: X[N, paths] 436 | X0 = starting point 437 | N = number of time points (there are N-1 time steps) 438 | T = Time in years 439 | paths = number of paths 440 | """ 441 | 442 | dt = T / (N - 1) 443 | X = np.zeros((N, paths)) 444 | X[0, :] = X0 445 | W = ss.norm.rvs(loc=0, scale=1, size=(N - 1, paths)) 446 | 447 | std_dt = np.sqrt(self.sigma**2 / (2 * self.kappa) * (1 - np.exp(-2 * self.kappa * dt))) 448 | for t in range(0, N - 1): 449 | X[t + 1, :] = self.theta + np.exp(-self.kappa * dt) * (X[t, :] - self.theta) + std_dt * W[t, :] 450 | 451 | return X 452 | -------------------------------------------------------------------------------- /src/FMNM/Solvers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sat Jul 27 11:13:45 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | from scipy import sparse 11 | from scipy.linalg import norm, solve_triangular 12 | from scipy.linalg.lapack import get_lapack_funcs 13 | from scipy.linalg.misc import LinAlgError 14 | 15 | 16 | def Thomas(A, b): 17 | """ 18 | Solver for the linear equation Ax=b using the Thomas algorithm. 19 | It is a wrapper of the LAPACK function dgtsv. 20 | """ 21 | 22 | D = A.diagonal(0) 23 | L = A.diagonal(-1) 24 | U = A.diagonal(1) 25 | 26 | if len(A.shape) != 2 or A.shape[0] != A.shape[1]: 27 | raise ValueError("expected square matrix") 28 | if A.shape[0] != b.shape[0]: 29 | raise ValueError("incompatible dimensions") 30 | 31 | (dgtsv,) = get_lapack_funcs(("gtsv",)) 32 | du2, d, du, x, info = dgtsv(L, D, U, b) 33 | 34 | if info == 0: 35 | return x 36 | if info > 0: 37 | raise LinAlgError("singular matrix: resolution failed at diagonal %d" % (info - 1)) 38 | 39 | 40 | def SOR(A, b, w=1, eps=1e-10, N_max=100): 41 | """ 42 | Solver for the linear equation Ax=b using the SOR algorithm. 43 | A = L + D + U 44 | Arguments: 45 | L = Strict Lower triangular matrix 46 | D = Diagonal 47 | U = Strict Upper triangular matrix 48 | w = Relaxation coefficient 49 | eps = tollerance 50 | N_max = Max number of iterations 51 | """ 52 | 53 | x0 = b.copy() # initial guess 54 | 55 | if sparse.issparse(A): 56 | D = sparse.diags(A.diagonal()) # diagonal 57 | U = sparse.triu(A, k=1) # Strict U 58 | L = sparse.tril(A, k=-1) # Strict L 59 | DD = (w * L + D).toarray() 60 | else: 61 | D = np.eye(A.shape[0]) * np.diag(A) # diagonal 62 | U = np.triu(A, k=1) # Strict U 63 | L = np.tril(A, k=-1) # Strict L 64 | DD = w * L + D 65 | 66 | for i in range(1, N_max + 1): 67 | x_new = solve_triangular(DD, (w * b - w * U @ x0 - (w - 1) * D @ x0), lower=True) 68 | if norm(x_new - x0) < eps: 69 | return x_new 70 | x0 = x_new 71 | if i == N_max: 72 | raise ValueError("Fail to converge in {} iterations".format(i)) 73 | 74 | 75 | def SOR2(A, b, w=1, eps=1e-10, N_max=100): 76 | """ 77 | Solver for the linear equation Ax=b using the SOR algorithm. 78 | It uses the coefficients and not the matrix multiplication. 79 | """ 80 | N = len(b) 81 | x0 = np.ones_like(b, dtype=np.float64) # initial guess 82 | x_new = np.ones_like(x0) # new solution 83 | 84 | for k in range(1, N_max + 1): 85 | for i in range(N): 86 | S = 0 87 | for j in range(N): 88 | if j != i: 89 | S += A[i, j] * x_new[j] 90 | x_new[i] = (1 - w) * x_new[i] + (w / A[i, i]) * (b[i] - S) 91 | 92 | if norm(x_new - x0) < eps: 93 | return x_new 94 | x0 = x_new.copy() 95 | if k == N_max: 96 | print("Fail to converge in {} iterations".format(k)) 97 | -------------------------------------------------------------------------------- /src/FMNM/TC_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jun 10 09:56:25 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | from time import time 10 | import numpy as np 11 | import numpy.matlib 12 | import FMNM.cost_utils as cost 13 | 14 | 15 | class TC_pricer: 16 | """ 17 | Solver for the option pricing model of Davis-Panas-Zariphopoulou. 18 | """ 19 | 20 | def __init__(self, Option_info, Process_info, cost_b=0, cost_s=0, gamma=0.001): 21 | """ 22 | Option_info: of type Option_param. It contains (S0,K,T) 23 | i.e. current price, strike, maturity in years 24 | 25 | Process_info: of type Diffusion_process. 26 | It contains (r,mu, sig) i.e. interest rate, drift coefficient, diffusion coeff 27 | cost_b: (lambda in the paper) BUY cost 28 | cost_s: (mu in the paper) SELL cost 29 | gamma: risk avversion coefficient 30 | """ 31 | 32 | if Option_info.payoff == "put": 33 | raise ValueError("Not implemented for Put Options") 34 | 35 | self.r = Process_info.r # interest rate 36 | self.mu = Process_info.mu # drift coefficient 37 | self.sig = Process_info.sig # diffusion coefficient 38 | self.S0 = Option_info.S0 # current price 39 | self.K = Option_info.K # strike 40 | self.T = Option_info.T # maturity in years 41 | self.cost_b = cost_b # (lambda in the paper) BUY cost 42 | self.cost_s = cost_s # (mu in the paper) SELL cost 43 | self.gamma = gamma # risk avversion coefficient 44 | 45 | def price(self, N=500, TYPE="writer", Time=False): 46 | """ 47 | N = number of time steps 48 | TYPE writer or buyer 49 | Time: Boolean 50 | """ 51 | t = time() # measures run time 52 | np.seterr(all="ignore") # ignore Warning for overflows 53 | 54 | x0 = np.log(self.S0) # current log-price 55 | T_vec, dt = np.linspace(0, self.T, N + 1, retstep=True) # vector of time steps and time steps 56 | delta = np.exp(-self.r * (self.T - T_vec)) # discount factor 57 | dx = self.sig * np.sqrt(dt) # space step1 58 | dy = dx # space step2 59 | M = int(np.floor(N / 2)) 60 | y = np.linspace(-M * dy, M * dy, 2 * M + 1) 61 | N_y = len(y) # dim of vector y 62 | med = np.where(y == 0)[0].item() # point where y==0 63 | 64 | def F(xx, ll, nn): 65 | return np.exp(self.gamma * (1 + self.cost_b) * np.exp(xx) * ll / delta[nn]) 66 | 67 | def G(xx, mm, nn): 68 | return np.exp(-self.gamma * (1 - self.cost_s) * np.exp(xx) * mm / delta[nn]) 69 | 70 | for portfolio in ["no_opt", TYPE]: 71 | # interates on the zero option and writer/buyer portfolios 72 | # Tree nodes at time N 73 | x = np.array([x0 + (self.mu - 0.5 * self.sig**2) * dt * N + (2 * i - N) * dx for i in range(N + 1)]) 74 | 75 | # Terminal conditions 76 | if portfolio == "no_opt": 77 | Q = np.exp(-self.gamma * cost.no_opt(x, y, self.cost_b, self.cost_s)) 78 | elif portfolio == "writer": 79 | Q = np.exp(-self.gamma * cost.writer(x, y, self.cost_b, self.cost_s, self.K)) 80 | elif portfolio == "buyer": 81 | Q = np.exp(-self.gamma * cost.buyer(x, y, self.cost_b, self.cost_s, self.K)) 82 | else: 83 | raise ValueError("TYPE can be only writer or buyer") 84 | 85 | for k in range(N - 1, -1, -1): 86 | # expectation term 87 | Q_new = (Q[:-1, :] + Q[1:, :]) / 2 88 | 89 | # create the logprice vector at time k 90 | x = np.array([x0 + (self.mu - 0.5 * self.sig**2) * dt * k + (2 * i - k) * dx for i in range(k + 1)]) 91 | 92 | # buy term 93 | Buy = np.copy(Q_new) 94 | Buy[:, :-1] = np.matlib.repmat(F(x, dy, k), N_y - 1, 1).T * Q_new[:, 1:] 95 | 96 | # sell term 97 | Sell = np.copy(Q_new) 98 | Sell[:, 1:] = np.matlib.repmat(G(x, dy, k), N_y - 1, 1).T * Q_new[:, :-1] 99 | 100 | # update the Q(:,:,k) 101 | Q = np.minimum(np.minimum(Buy, Sell), Q_new) 102 | 103 | if portfolio == "no_opt": 104 | Q_no = Q[0, med] 105 | else: 106 | Q_yes = Q[0, med] 107 | 108 | if TYPE == "writer": 109 | price = (delta[0] / self.gamma) * np.log(Q_yes / Q_no) 110 | else: 111 | price = (delta[0] / self.gamma) * np.log(Q_no / Q_yes) 112 | 113 | if Time is True: 114 | elapsed = time() - t 115 | return price, elapsed 116 | else: 117 | return price 118 | -------------------------------------------------------------------------------- /src/FMNM/VG_pricer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Aug 12 18:47:05 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | from scipy import sparse 10 | from scipy.sparse.linalg import splu 11 | from time import time 12 | import numpy as np 13 | import scipy as scp 14 | from scipy import signal 15 | from scipy.integrate import quad 16 | import scipy.stats as ss 17 | import scipy.special as scps 18 | 19 | import matplotlib.pyplot as plt 20 | from matplotlib import cm 21 | from FMNM.CF import cf_VG 22 | from FMNM.probabilities import Q1, Q2 23 | from functools import partial 24 | from FMNM.FFT import fft_Lewis, IV_from_Lewis 25 | 26 | 27 | class VG_pricer: 28 | """ 29 | Closed Formula. 30 | Monte Carlo. 31 | Finite-difference PIDE: Explicit-implicit scheme, with Brownian approximation 32 | 33 | 0 = dV/dt + (r -(1/2)sig^2 -w) dV/dx + (1/2)sig^2 d^V/dx^2 34 | + \int[ V(x+y) nu(dy) ] -(r+lam)V 35 | """ 36 | 37 | def __init__(self, Option_info, Process_info): 38 | """ 39 | Process_info: of type VG_process. 40 | It contains the interest rate r and the VG parameters (sigma, theta, kappa) 41 | 42 | Option_info: of type Option_param. 43 | It contains (S0,K,T) i.e. current price, strike, maturity in years 44 | """ 45 | self.r = Process_info.r # interest rate 46 | self.sigma = Process_info.sigma # VG parameter 47 | self.theta = Process_info.theta # VG parameter 48 | self.kappa = Process_info.kappa # VG parameter 49 | self.exp_RV = Process_info.exp_RV # function to generate exponential VG Random Variables 50 | self.w = -np.log(1 - self.theta * self.kappa - self.kappa / 2 * self.sigma**2) / self.kappa # coefficient w 51 | 52 | self.S0 = Option_info.S0 # current price 53 | self.K = Option_info.K # strike 54 | self.T = Option_info.T # maturity in years 55 | 56 | self.price = 0 57 | self.S_vec = None 58 | self.price_vec = None 59 | self.mesh = None 60 | self.exercise = Option_info.exercise 61 | self.payoff = Option_info.payoff 62 | 63 | def payoff_f(self, S): 64 | if self.payoff == "call": 65 | Payoff = np.maximum(S - self.K, 0) 66 | elif self.payoff == "put": 67 | Payoff = np.maximum(self.K - S, 0) 68 | return Payoff 69 | 70 | def closed_formula(self): 71 | """ 72 | VG closed formula. Put is obtained by put/call parity. 73 | """ 74 | 75 | def Psy(a, b, g): 76 | f = lambda u: ss.norm.cdf(a / np.sqrt(u) + b * np.sqrt(u)) * u ** (g - 1) * np.exp(-u) / scps.gamma(g) 77 | result = quad(f, 0, np.inf) 78 | return result[0] 79 | 80 | # Ugly parameters 81 | xi = -self.theta / self.sigma**2 82 | s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2)) 83 | alpha = xi * s 84 | 85 | c1 = self.kappa / 2 * (alpha + s) ** 2 86 | c2 = self.kappa / 2 * alpha**2 87 | d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2))) 88 | 89 | # Closed formula 90 | call = self.S0 * Psy( 91 | d * np.sqrt((1 - c1) / self.kappa), 92 | (alpha + s) * np.sqrt(self.kappa / (1 - c1)), 93 | self.T / self.kappa, 94 | ) - self.K * np.exp(-self.r * self.T) * Psy( 95 | d * np.sqrt((1 - c2) / self.kappa), 96 | (alpha) * np.sqrt(self.kappa / (1 - c2)), 97 | self.T / self.kappa, 98 | ) 99 | 100 | if self.payoff == "call": 101 | return call 102 | elif self.payoff == "put": 103 | return call - self.S0 + self.K * np.exp(-self.r * self.T) 104 | else: 105 | raise ValueError("invalid type. Set 'call' or 'put'") 106 | 107 | def Fourier_inversion(self): 108 | """ 109 | Price obtained by inversion of the characteristic function 110 | """ 111 | k = np.log(self.K / self.S0) # log moneyness 112 | cf_VG_b = partial( 113 | cf_VG, 114 | t=self.T, 115 | mu=(self.r - self.w), 116 | theta=self.theta, 117 | sigma=self.sigma, 118 | kappa=self.kappa, 119 | ) 120 | 121 | right_lim = 5000 # using np.inf may create warnings 122 | if self.payoff == "call": 123 | call = self.S0 * Q1(k, cf_VG_b, right_lim) - self.K * np.exp(-self.r * self.T) * Q2( 124 | k, cf_VG_b, right_lim 125 | ) # pricing function 126 | return call 127 | elif self.payoff == "put": 128 | put = self.K * np.exp(-self.r * self.T) * (1 - Q2(k, cf_VG_b, right_lim)) - self.S0 * ( 129 | 1 - Q1(k, cf_VG_b, right_lim) 130 | ) # pricing function 131 | return put 132 | else: 133 | raise ValueError("invalid type. Set 'call' or 'put'") 134 | 135 | def MC(self, N, Err=False, Time=False): 136 | """ 137 | Variance Gamma Monte Carlo 138 | Err = return Standard Error if True 139 | Time = return execution time if True 140 | """ 141 | t_init = time() 142 | 143 | S_T = self.exp_RV(self.S0, self.T, N) 144 | V = scp.mean(np.exp(-self.r * self.T) * self.payoff_f(S_T), axis=0) 145 | 146 | if Err is True: 147 | if Time is True: 148 | elapsed = time() - t_init 149 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)), elapsed 150 | else: 151 | return V, ss.sem(np.exp(-self.r * self.T) * self.payoff_f(S_T)) 152 | else: 153 | if Time is True: 154 | elapsed = time() - t_init 155 | return V, elapsed 156 | else: 157 | return V 158 | 159 | def FFT(self, K): 160 | """ 161 | FFT method. It returns a vector of prices. 162 | K is an array of strikes 163 | """ 164 | K = np.array(K) 165 | cf_VG_b = partial( 166 | cf_VG, 167 | t=self.T, 168 | mu=(self.r - self.w), 169 | theta=self.theta, 170 | sigma=self.sigma, 171 | kappa=self.kappa, 172 | ) 173 | 174 | if self.payoff == "call": 175 | return fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic") 176 | elif self.payoff == "put": # put-call parity 177 | return ( 178 | fft_Lewis(K, self.S0, self.r, self.T, cf_VG_b, interp="cubic") - self.S0 + K * np.exp(-self.r * self.T) 179 | ) 180 | else: 181 | raise ValueError("invalid type. Set 'call' or 'put'") 182 | 183 | def IV_Lewis(self): 184 | """Implied Volatility from the Lewis formula""" 185 | 186 | cf_VG_b = partial( 187 | cf_VG, 188 | t=self.T, 189 | mu=(self.r - self.w), 190 | theta=self.theta, 191 | sigma=self.sigma, 192 | kappa=self.kappa, 193 | ) 194 | 195 | if self.payoff == "call": 196 | return IV_from_Lewis(self.K, self.S0, self.T, self.r, cf_VG_b) 197 | elif self.payoff == "put": 198 | raise NotImplementedError 199 | else: 200 | raise ValueError("invalid type. Set 'call' or 'put'") 201 | 202 | def PIDE_price(self, steps, Time=False): 203 | """ 204 | steps = tuple with number of space steps and time steps 205 | payoff = "call" or "put" 206 | exercise = "European" or "American" 207 | Time = Boolean. Execution time. 208 | """ 209 | t_init = time() 210 | 211 | Nspace = steps[0] 212 | Ntime = steps[1] 213 | 214 | S_max = 6 * float(self.K) 215 | S_min = float(self.K) / 6 216 | x_max = np.log(S_max) 217 | x_min = np.log(S_min) 218 | 219 | dev_X = np.sqrt(self.sigma**2 + self.theta**2 * self.kappa) # std dev VG process 220 | 221 | dx = (x_max - x_min) / (Nspace - 1) 222 | extraP = int(np.floor(5 * dev_X / dx)) # extra points beyond the B.C. 223 | x = np.linspace(x_min - extraP * dx, x_max + extraP * dx, Nspace + 2 * extraP) # space discretization 224 | t, dt = np.linspace(0, self.T, Ntime, retstep=True) # time discretization 225 | 226 | Payoff = self.payoff_f(np.exp(x)) 227 | offset = np.zeros(Nspace - 2) 228 | V = np.zeros((Nspace + 2 * extraP, Ntime)) # grid initialization 229 | 230 | if self.payoff == "call": 231 | V[:, -1] = Payoff # terminal conditions 232 | V[-extraP - 1 :, :] = np.exp(x[-extraP - 1 :]).reshape(extraP + 1, 1) * np.ones( 233 | (extraP + 1, Ntime) 234 | ) - self.K * np.exp(-self.r * t[::-1]) * np.ones( 235 | (extraP + 1, Ntime) 236 | ) # boundary condition 237 | V[: extraP + 1, :] = 0 238 | else: 239 | V[:, -1] = Payoff 240 | V[-extraP - 1 :, :] = 0 241 | V[: extraP + 1, :] = self.K * np.exp(-self.r * t[::-1]) * np.ones((extraP + 1, Ntime)) 242 | 243 | A = self.theta / (self.sigma**2) 244 | B = np.sqrt(self.theta**2 + 2 * self.sigma**2 / self.kappa) / self.sigma**2 245 | 246 | def levy_m(y): 247 | """Levy measure VG""" 248 | return np.exp(A * y - B * np.abs(y)) / (self.kappa * np.abs(y)) 249 | 250 | eps = 1.5 * dx # the cutoff near 0 251 | lam = ( 252 | quad(levy_m, -(extraP + 1.5) * dx, -eps)[0] + quad(levy_m, eps, (extraP + 1.5) * dx)[0] 253 | ) # approximated intensity 254 | 255 | def int_w(y): 256 | """integrator""" 257 | return (np.exp(y) - 1) * levy_m(y) 258 | 259 | int_s = lambda y: np.abs(y) * np.exp(A * y - B * np.abs(y)) / self.kappa # avoid division by zero 260 | 261 | w = ( 262 | quad(int_w, -(extraP + 1.5) * dx, -eps)[0] + quad(int_w, eps, (extraP + 1.5) * dx)[0] 263 | ) # is the approx of omega 264 | 265 | sig2 = quad(int_s, -eps, eps)[0] # the small jumps variance 266 | 267 | dxx = dx * dx 268 | a = (dt / 2) * ((self.r - w - 0.5 * sig2) / dx - sig2 / dxx) 269 | b = 1 + dt * (sig2 / dxx + self.r + lam) 270 | c = -(dt / 2) * ((self.r - w - 0.5 * sig2) / dx + sig2 / dxx) 271 | D = sparse.diags([a, b, c], [-1, 0, 1], shape=(Nspace - 2, Nspace - 2)).tocsc() 272 | DD = splu(D) 273 | 274 | nu = np.zeros(2 * extraP + 3) # Lévy measure vector 275 | x_med = extraP + 1 # middle point in nu vector 276 | x_nu = np.linspace(-(extraP + 1 + 0.5) * dx, (extraP + 1 + 0.5) * dx, 2 * (extraP + 2)) # integration domain 277 | for i in range(len(nu)): 278 | if (i == x_med) or (i == x_med - 1) or (i == x_med + 1): 279 | continue 280 | nu[i] = quad(levy_m, x_nu[i], x_nu[i + 1])[0] 281 | 282 | if self.exercise == "European": 283 | # Backward iteration 284 | for i in range(Ntime - 2, -1, -1): 285 | offset[0] = a * V[extraP, i] 286 | offset[-1] = c * V[-1 - extraP, i] 287 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 288 | V[:, i + 1], nu[::-1], mode="valid", method="auto" 289 | ) 290 | V[extraP + 1 : -extraP - 1, i] = DD.solve(V_jump - offset) 291 | elif self.exercise == "American": 292 | for i in range(Ntime - 2, -1, -1): 293 | offset[0] = a * V[extraP, i] 294 | offset[-1] = c * V[-1 - extraP, i] 295 | V_jump = V[extraP + 1 : -extraP - 1, i + 1] + dt * signal.convolve( 296 | V[:, i + 1], nu[::-1], mode="valid", method="auto" 297 | ) 298 | V[extraP + 1 : -extraP - 1, i] = np.maximum(DD.solve(V_jump - offset), Payoff[extraP + 1 : -extraP - 1]) 299 | 300 | X0 = np.log(self.S0) # current log-price 301 | self.S_vec = np.exp(x[extraP + 1 : -extraP - 1]) # vector of S 302 | self.price = np.interp(X0, x, V[:, 0]) 303 | self.price_vec = V[extraP + 1 : -extraP - 1, 0] 304 | self.mesh = V[extraP + 1 : -extraP - 1, :] 305 | 306 | if Time is True: 307 | elapsed = time() - t_init 308 | return self.price, elapsed 309 | else: 310 | return self.price 311 | 312 | def plot(self, axis=None): 313 | if type(self.S_vec) != np.ndarray or type(self.price_vec) != np.ndarray: 314 | self.PIDE_price((5000, 4000)) 315 | 316 | plt.plot(self.S_vec, self.payoff_f(self.S_vec), color="blue", label="Payoff") 317 | plt.plot(self.S_vec, self.price_vec, color="red", label="VG curve") 318 | if type(axis) == list: 319 | plt.axis(axis) 320 | plt.xlabel("S") 321 | plt.ylabel("price") 322 | plt.title("VG price") 323 | plt.legend(loc="upper left") 324 | plt.show() 325 | 326 | def mesh_plt(self): 327 | if type(self.S_vec) != np.ndarray or type(self.mesh) != np.ndarray: 328 | self.PDE_price((7000, 5000)) 329 | 330 | fig = plt.figure() 331 | ax = fig.add_subplot(111, projection="3d") 332 | 333 | X, Y = np.meshgrid(np.linspace(0, self.T, self.mesh.shape[1]), self.S_vec) 334 | ax.plot_surface(Y, X, self.mesh, cmap=cm.ocean) 335 | ax.set_title("VG price surface") 336 | ax.set_xlabel("S") 337 | ax.set_ylabel("t") 338 | ax.set_zlabel("V") 339 | ax.view_init(30, -100) # this function rotates the 3d plot 340 | plt.show() 341 | 342 | def closed_formula_wrong(self): 343 | """ 344 | VG closed formula. This implementation seems correct, BUT IT DOES NOT WORK!! 345 | Here I use the closed formula of Carr,Madan,Chang 1998. 346 | With scps.kv, a modified Bessel function of second kind. 347 | You can try to run it, but the output is slightly different from expected. 348 | """ 349 | 350 | def Phi(alpha, beta, gamm, x, y): 351 | f = lambda u: u ** (alpha - 1) * (1 - u) ** (gamm - alpha - 1) * (1 - u * x) ** (-beta) * np.exp(u * y) 352 | result = quad(f, 0.00000001, 0.99999999) 353 | return (scps.gamma(gamm) / (scps.gamma(alpha) * scps.gamma(gamm - alpha))) * result[0] 354 | 355 | def Psy(a, b, g): 356 | c = np.abs(a) * np.sqrt(2 + b**2) 357 | u = b / np.sqrt(2 + b**2) 358 | 359 | value = ( 360 | (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** g) 361 | / (np.sqrt(2 * np.pi) * g * scps.gamma(g)) 362 | * scps.kv(g + 0.5, c) 363 | * Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u)) 364 | - np.sign(a) 365 | * (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g)) 366 | / (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g)) 367 | * scps.kv(g - 0.5, c) 368 | * Phi(g + 1, 1 - g, 2 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u)) 369 | + np.sign(a) 370 | * (c ** (g + 0.5) * np.exp(np.sign(a) * c) * (1 + u) ** (1 + g)) 371 | / (np.sqrt(2 * np.pi) * (g + 1) * scps.gamma(g)) 372 | * scps.kv(g - 0.5, c) 373 | * Phi(g, 1 - g, 1 + g, (1 + u) / 2, -np.sign(a) * c * (1 + u)) 374 | ) 375 | return value 376 | 377 | # Ugly parameters 378 | xi = -self.theta / self.sigma**2 379 | s = self.sigma / np.sqrt(1 + ((self.theta / self.sigma) ** 2) * (self.kappa / 2)) 380 | alpha = xi * s 381 | 382 | c1 = self.kappa / 2 * (alpha + s) ** 2 383 | c2 = self.kappa / 2 * alpha**2 384 | d = 1 / s * (np.log(self.S0 / self.K) + self.r * self.T + self.T / self.kappa * np.log((1 - c1) / (1 - c2))) 385 | 386 | # Closed formula 387 | call = self.S0 * Psy( 388 | d * np.sqrt((1 - c1) / self.kappa), 389 | (alpha + s) * np.sqrt(self.kappa / (1 - c1)), 390 | self.T / self.kappa, 391 | ) - self.K * np.exp(-self.r * self.T) * Psy( 392 | d * np.sqrt((1 - c2) / self.kappa), 393 | (alpha) * np.sqrt(self.kappa / (1 - c2)), 394 | self.T / self.kappa, 395 | ) 396 | 397 | return call 398 | -------------------------------------------------------------------------------- /src/FMNM/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sat Aug 5 16:35:17 2023 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | 10 | __all__ = [ 11 | "cython", 12 | "BS_pricer", 13 | "CF", 14 | "cost_utils", 15 | "FFT", 16 | "Heston_pricer", 17 | "Kalman_filter", 18 | "Merton_pricer", 19 | "NIG_pricer", 20 | "Parameters", 21 | "portfolio_optimization", 22 | "probabilities", 23 | "Processes", 24 | "Solvers", 25 | "TC_pricer", 26 | "VG_pricer", 27 | ] 28 | 29 | from FMNM import * 30 | -------------------------------------------------------------------------------- /src/FMNM/cost_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jun 10 09:56:25 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | 11 | 12 | def no_opt(x, y, cost_b, cost_s): 13 | cost = np.zeros((len(x), len(y))) 14 | 15 | for i in range(len(y)): 16 | if y[i] <= 0: 17 | cost[:, i] = (1 + cost_b) * y[i] * np.exp(x) 18 | else: 19 | cost[:, i] = (1 - cost_s) * y[i] * np.exp(x) 20 | 21 | return cost 22 | 23 | 24 | def writer(x, y, cost_b, cost_s, K): 25 | cost = np.zeros((len(x), len(y))) 26 | 27 | for i in range(len(x)): 28 | for j in range(len(y)): 29 | if y[j] < 0 and (1 + cost_b) * np.exp(x[i]) <= K: 30 | cost[i][j] = (1 + cost_b) * y[j] * np.exp(x[i]) 31 | 32 | elif y[j] >= 0 and (1 + cost_b) * np.exp(x[i]) <= K: 33 | cost[i][j] = (1 - cost_s) * y[j] * np.exp(x[i]) 34 | 35 | elif y[j] - 1 >= 0 and (1 + cost_b) * np.exp(x[i]) > K: 36 | cost[i][j] = ((1 - cost_s) * (y[j] - 1) * np.exp(x[i])) + K 37 | 38 | elif y[j] - 1 < 0 and (1 + cost_b) * np.exp(x[i]) > K: 39 | cost[i][j] = ((1 + cost_b) * (y[j] - 1) * np.exp(x[i])) + K 40 | 41 | return cost 42 | 43 | 44 | def buyer(x, y, cost_b, cost_s, K): 45 | cost = np.zeros((len(x), len(y))) 46 | 47 | for i in range(len(x)): 48 | for j in range(len(y)): 49 | if y[j] < 0 and (1 + cost_b) * np.exp(x[i]) <= K: 50 | cost[i][j] = (1 + cost_b) * y[j] * np.exp(x[i]) 51 | 52 | elif y[j] >= 0 and (1 + cost_b) * np.exp(x[i]) <= K: 53 | cost[i][j] = (1 - cost_s) * y[j] * np.exp(x[i]) 54 | 55 | elif y[j] + 1 >= 0 and (1 + cost_b) * np.exp(x[i]) > K: 56 | cost[i][j] = ((1 - cost_s) * (y[j] + 1) * np.exp(x[i])) - K 57 | 58 | elif y[j] + 1 < 0 and (1 + cost_b) * np.exp(x[i]) > K: 59 | cost[i][j] = ((1 + cost_b) * (y[j] + 1) * np.exp(x[i])) - K 60 | 61 | return cost 62 | -------------------------------------------------------------------------------- /src/FMNM/cython/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantaro86/Financial-Models-Numerical-Methods/585ed19462665bd822c88b5b6e34fca8ad441370/src/FMNM/cython/__init__.py -------------------------------------------------------------------------------- /src/FMNM/cython/heston.pyx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Tue Oct 15 17:46:10 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | import scipy.stats as ss 11 | cimport numpy as np 12 | cimport cython 13 | from libc.math cimport isnan 14 | 15 | 16 | cdef extern from "math.h": 17 | double sqrt(double m) 18 | double exp(double m) 19 | double log(double m) 20 | double fabs(double m) 21 | 22 | 23 | 24 | @cython.boundscheck(False) # turn off bounds-checking for entire function 25 | @cython.wraparound(False) # turn off negative index wrapping for entire function 26 | cpdef Heston_paths(int N, int paths, double T, double S0, double v0, 27 | double mu, double rho, double kappa, double theta, double sigma ): 28 | """ 29 | Generates random values of stock S and variance v at maturity T. 30 | This function uses the "abs" method for the variance. 31 | 32 | OUTPUT: 33 | Two arrays of size equal to "paths". 34 | 35 | int N = time steps 36 | int paths = number of paths 37 | double T = maturity 38 | double S0 = spot price 39 | double v0 = spot variance 40 | double mu = drift 41 | double rho = correlation coefficient 42 | double kappa = mean reversion coefficient 43 | double theta = long-term variance 44 | double sigma = Vol of Vol - Volatility of instantaneous variance 45 | """ 46 | 47 | cdef double dt = T/(N-1) 48 | cdef double dt_sq = sqrt(dt) 49 | 50 | assert(2*kappa * theta > sigma**2) # Feller condition 51 | 52 | cdef double[:] W_S # declaration Brownian motion for S 53 | cdef double[:] W_v # declaration Brownian motion for v 54 | 55 | # Initialize 56 | cdef double[:] v_T = np.zeros(paths) # values of v at T 57 | cdef double[:] S_T = np.zeros(paths) # values of S at T 58 | cdef double[:] v = np.zeros(N) 59 | cdef double[:] S = np.zeros(N) 60 | 61 | cdef int t, path 62 | for path in range(paths): 63 | # Generate random Brownian Motions 64 | W_S_arr = np.random.normal(loc=0, scale=1, size=N-1 ) 65 | W_v_arr = rho * W_S_arr + sqrt(1-rho**2) * np.random.normal(loc=0, scale=1, size=N-1 ) 66 | W_S = W_S_arr 67 | W_v = W_v_arr 68 | S[0] = S0 # stock at 0 69 | v[0] = v0 # variance at 0 70 | 71 | for t in range(0,N-1): 72 | v[t+1] = fabs( v[t] + kappa*(theta - v[t])*dt + sigma * sqrt(v[t]) * dt_sq * W_v[t] ) 73 | S[t+1] = S[t] * exp( (mu - 0.5*v[t])*dt + sqrt(v[t]) * dt_sq * W_S[t] ) 74 | 75 | S_T[path] = S[N-1] 76 | v_T[path] = v[N-1] 77 | 78 | return np.asarray(S_T), np.asarray(v_T) 79 | 80 | 81 | 82 | 83 | cpdef Heston_paths_log(int N, int paths, double T, double S0, double v0, 84 | double mu, double rho, double kappa, double theta, double sigma ): 85 | """ 86 | Generates random values of stock S and variance v at maturity T. 87 | This function uses the log-variables. NaN and abnormal numbers are ignored. 88 | 89 | OUTPUT: 90 | Two arrays of size smaller or equal of "paths". 91 | 92 | INPUT: 93 | int N = time steps 94 | int paths = number of paths 95 | double T = maturity 96 | double S0 = spot price 97 | double v0 = spot variance 98 | double mu = drift 99 | double rho = correlation coefficient 100 | double kappa = mean reversion coefficient 101 | double theta = long-term variance 102 | double sigma = Vol of Vol - Volatility of instantaneous variance 103 | """ 104 | 105 | cdef double dt = T/(N-1) 106 | cdef double dt_sq = sqrt(dt) 107 | 108 | cdef double X0 = log(S0) # log price 109 | cdef double Y0 = log(v0) # log-variance 110 | 111 | assert(2*kappa * theta > sigma**2) # Feller condition 112 | cdef double std_asy = sqrt( theta * sigma**2 /(2*kappa) ) 113 | 114 | cdef double[:] W_S # declaration Brownian motion for S 115 | cdef double[:] W_v # declaration Brownian motion for v 116 | 117 | # Initialize 118 | cdef double[:] Y_T = np.zeros(paths) 119 | cdef double[:] X_T = np.zeros(paths) 120 | cdef double[:] Y = np.zeros(N) 121 | cdef double[:] X = np.zeros(N) 122 | 123 | cdef int t, path 124 | cdef double v, v_sq 125 | cdef double up_bound = log( (theta + 10*std_asy) ) # mean + 10 standard deviations 126 | cdef int warning = 0 127 | cdef int counter = 0 128 | 129 | # Generate paths 130 | for path in range(paths): 131 | # Generate random Brownian Motions 132 | W_S_arr = np.random.normal(loc=0, scale=1, size=N-1 ) 133 | W_v_arr = rho * W_S_arr + sqrt(1-rho**2) * np.random.normal(loc=0, scale=1, size=N-1 ) 134 | W_S = W_S_arr 135 | W_v = W_v_arr 136 | X[0] = X0 # log-stock 137 | Y[0] = Y0 # log-variance 138 | 139 | for t in range(0,N-1): 140 | v = exp(Y[t]) # variance 141 | v_sq = sqrt(v) # square root of variance 142 | 143 | Y[t+1] = Y[t] + (1/v)*( kappa*(theta - v) - 0.5*sigma**2 )*dt + sigma * (1/v_sq) * dt_sq * W_v[t] 144 | X[t+1] = X[t] + (mu - 0.5*v)*dt + v_sq * dt_sq * W_S[t] 145 | 146 | if ( Y[-1] > up_bound or isnan(Y[-1]) ): 147 | warning = 1 148 | counter += 1 149 | X_T[path] = 10000 150 | Y_T[path] = 10000 151 | continue 152 | 153 | X_T[path] = X[-1] 154 | Y_T[path] = Y[-1] 155 | 156 | if (warning==1): 157 | print("WARNING. ", counter, " paths have been removed because of the overflow.") 158 | print("SOLUTION: Use a bigger value N.") 159 | 160 | Y_arr = np.asarray(Y_T) 161 | Y_good = Y_arr[ Y_arr < up_bound ] 162 | X_good = np.asarray(X_T)[ Y_arr < up_bound ] 163 | 164 | return np.exp(X_good), np.exp(Y_good) 165 | -------------------------------------------------------------------------------- /src/FMNM/cython/solvers.pyx: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Mon Jul 29 11:13:45 2019 3 | 4 | @author: cantaro86 5 | """ 6 | 7 | import numpy as np 8 | from scipy.linalg import norm 9 | cimport numpy as np 10 | cimport cython 11 | 12 | cdef np.float64_t distance2(np.float64_t[:] a, np.float64_t[:] b, unsigned int N): 13 | cdef np.float64_t dist = 0 14 | cdef unsigned int i 15 | for i in range(N): 16 | dist += (a[i] - b[i]) * (a[i] - b[i]) 17 | return dist 18 | 19 | 20 | @cython.boundscheck(False) 21 | @cython.wraparound(False) 22 | def SOR(np.float64_t aa, 23 | np.float64_t bb, np.float64_t cc, 24 | np.float64_t[:] b, 25 | np.float64_t w=1, np.float64_t eps=1e-10, unsigned int N_max = 500): 26 | 27 | cdef unsigned int N = b.size 28 | 29 | cdef np.float64_t[:] x0 = np.ones(N, dtype=np.float64) # initial guess 30 | cdef np.float64_t[:] x_new = np.ones(N, dtype=np.float64) # new solution 31 | 32 | 33 | cdef unsigned int i, k 34 | cdef np.float64_t S 35 | 36 | for k in range(1,N_max+1): 37 | for i in range(N): 38 | if (i==0): 39 | S = cc * x_new[1] 40 | elif (i==N-1): 41 | S = aa * x_new[N-2] 42 | else: 43 | S = aa * x_new[i-1] + cc * x_new[i+1] 44 | x_new[i] = (1-w)*x_new[i] + (w/bb) * (b[i] - S) 45 | if distance2(x_new, x0, N) < eps*eps: 46 | return x_new 47 | x0[:] = x_new 48 | if k==N_max: 49 | print("Fail to converge in {} iterations".format(k)) 50 | return x_new 51 | 52 | 53 | @cython.boundscheck(False) 54 | @cython.wraparound(False) 55 | def PSOR(np.float64_t aa, 56 | np.float64_t bb, np.float64_t cc, 57 | np.float64_t[:] B, np.float64_t[:] C, 58 | np.float64_t w=1, np.float64_t eps=1e-10, unsigned int N_max = 500): 59 | 60 | cdef unsigned int N = B.size 61 | 62 | cdef np.float64_t[:] x0 = np.ones(N, dtype=np.float64) # initial guess 63 | cdef np.float64_t[:] x_new = np.ones(N, dtype=np.float64) # new solution 64 | 65 | cdef unsigned int i, k 66 | cdef np.float64_t S 67 | 68 | for k in range(1,N_max+1): 69 | for i in range(N): 70 | if (i==0): 71 | S = cc * x_new[1] 72 | elif (i==N-1): 73 | S = aa * x_new[N-2] 74 | else: 75 | S = aa * x_new[i-1] + cc * x_new[i+1] 76 | x_new[i] = (1-w)*x_new[i] + (w/bb) * (B[i] - S) 77 | x_new[i] = x_new[i] if (x_new[i] > C[i]) else C[i] 78 | 79 | if distance2(x_new, x0, N) < eps*eps: 80 | print("Convergence after {} iterations".format(k)) 81 | return x_new 82 | x0[:] = x_new 83 | if k==N_max: 84 | print("Fail to converge in {} iterations".format(k)) 85 | return x_new 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/FMNM/portfolio_optimization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import minimize, Bounds, LinearConstraint 3 | 4 | 5 | def optimal_weights(MU, COV, Rf=0, w_max=1, desired_mean=None, desired_std=None): 6 | """ 7 | Compute the optimal weights for a portfolio containing a risk free asset and stocks. 8 | MU = vector of mean 9 | COV = covariance matrix 10 | Rf = risk free return 11 | w_max = maximum weight bound for the stock portfolio 12 | desired_mean = desired mean of the portfolio 13 | desired_std = desired standard deviation of the portfolio 14 | """ 15 | 16 | if (desired_mean is not None) and (desired_std is not None): 17 | raise ValueError("One among desired_mean and desired_std must be None") 18 | if ((desired_mean is not None) or (desired_std is not None)) and Rf == 0: 19 | raise ValueError("We just optimize the Sharpe ratio, no computation of efficient frontier") 20 | 21 | N = len(MU) 22 | bounds = Bounds(0, w_max) 23 | linear_constraint = LinearConstraint(np.ones(N, dtype=int), 1, 1) 24 | weights = np.ones(N) 25 | x0 = weights / np.sum(weights) # initial guess 26 | 27 | def sharpe_fun(w): 28 | return -(MU @ w - Rf) / np.sqrt(w.T @ COV @ w) 29 | 30 | res = minimize( 31 | sharpe_fun, 32 | x0=x0, 33 | method="trust-constr", 34 | constraints=linear_constraint, 35 | bounds=bounds, 36 | ) 37 | print(res.message + "\n") 38 | w_sr = res.x 39 | std_stock_portf = np.sqrt(w_sr @ COV @ w_sr) 40 | mean_stock_portf = MU @ w_sr 41 | stock_port_results = { 42 | "Sharpe Ratio": -sharpe_fun(w_sr), 43 | "stock weights": w_sr.round(4), 44 | "stock portfolio": { 45 | "std": std_stock_portf.round(6), 46 | "mean": mean_stock_portf.round(6), 47 | }, 48 | } 49 | 50 | if (desired_mean is None) and (desired_std is None): 51 | return stock_port_results 52 | 53 | elif (desired_mean is None) and (desired_std is not None): 54 | w_stock = desired_std / std_stock_portf 55 | if desired_std > std_stock_portf: 56 | print( 57 | "The risk you take is higher than the tangency portfolio risk \ 58 | ==> SHORT POSTION" 59 | ) 60 | tot_port_mean = Rf + w_stock * (mean_stock_portf - Rf) 61 | return { 62 | **stock_port_results, 63 | "Bond + Stock weights": { 64 | "Bond": (1 - w_stock).round(4), 65 | "Stock": w_stock.round(4), 66 | }, 67 | "Total portfolio": {"std": desired_std, "mean": tot_port_mean.round(6)}, 68 | } 69 | 70 | elif (desired_mean is not None) and (desired_std is None): 71 | w_stock = (desired_mean - Rf) / (mean_stock_portf - Rf) 72 | if desired_mean > mean_stock_portf: 73 | print( 74 | "The return you want is higher than the tangency portfolio return \ 75 | ==> SHORT POSTION" 76 | ) 77 | tot_port_std = w_stock * std_stock_portf 78 | return { 79 | **stock_port_results, 80 | "Bond + Stock weights": { 81 | "Bond": (1 - w_stock).round(4), 82 | "Stock": w_stock.round(4), 83 | }, 84 | "Total portfolio": {"std": tot_port_std.round(6), "mean": desired_mean}, 85 | } 86 | -------------------------------------------------------------------------------- /src/FMNM/probabilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Oct 7 18:33:39 2019 5 | 6 | @author: cantaro86 7 | """ 8 | 9 | import numpy as np 10 | from scipy.integrate import quad 11 | from functools import partial 12 | from FMNM.CF import cf_Heston_good 13 | import scipy.special as scps 14 | from math import factorial 15 | 16 | 17 | def Q1(k, cf, right_lim): 18 | """ 19 | P(X