├── .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