├── files ├── py4fi.pdf ├── basic-docinfo.html ├── images │ └── spx_volatility.png ├── py4fi.asciidoc ├── code │ ├── bsm_mcs_euro.py │ ├── why_python.txt │ ├── why_python.txt~ │ └── why_python.ipynb ├── nb_parse.py ├── basic-theme.yml ├── why_python.asciidoc └── py4fi.html ├── output ├── tpqad_output_01.png └── tpqad_output_02.png ├── LICENSE.txt ├── .gitignore ├── README.md └── tpqad.ipynb /files/py4fi.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/tpqad/master/files/py4fi.pdf -------------------------------------------------------------------------------- /files/basic-docinfo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /output/tpqad_output_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/tpqad/master/output/tpqad_output_01.png -------------------------------------------------------------------------------- /output/tpqad_output_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/tpqad/master/output/tpqad_output_02.png -------------------------------------------------------------------------------- /files/images/spx_volatility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhilpisch/tpqad/master/files/images/spx_volatility.png -------------------------------------------------------------------------------- /files/py4fi.asciidoc: -------------------------------------------------------------------------------- 1 | = Python for Finance 2 | Dr. Yves J. Hilpisch | 3 | The Python Quants GmbH | www.tpq.io 4 | :doctype: book 5 | :source-highlighter: coderay 6 | // :coderay-css: inline 7 | :listing-caption: Listing 8 | :pdf-page-size: A4 9 | :icons: font 10 | :toc: left 11 | :toclevels: 2 12 | :numbered: 13 | :auto-fit: 14 | :docinfo: 15 | // :imagesdir: images 16 | 17 | 18 | include::why_python.asciidoc[] 19 | -------------------------------------------------------------------------------- /files/code/bsm_mcs_euro.py: -------------------------------------------------------------------------------- 1 | # 2 | # Monte Carlo valuation of European call option 3 | # in Black-Scholes-Merton model 4 | # bsm_mcs_euro.py 5 | # 6 | # Python for Finance, 2nd ed. 7 | # (c) Dr. Yves J. Hilpisch 8 | # 9 | import math 10 | import numpy as np 11 | 12 | # Parameter Values 13 | S0 = 100. # initial index level 14 | K = 105. # strike price 15 | T = 1.0 # time-to-maturity 16 | r = 0.05 # riskless short rate 17 | sigma = 0.2 # volatility 18 | 19 | I = 100000 # number of simulations 20 | 21 | # Valuation Algorithm 22 | z = np.random.standard_normal(I) # pseudo-random numbers 23 | # index values at maturity 24 | ST = S0 * np.exp((r - 0.5 * sigma ** 2) * T + sigma * math.sqrt(T) * z) 25 | hT = np.maximum(ST - K, 0) # payoff at maturity 26 | C0 = math.exp(-r * T) * np.mean(hT) # Monte Carlo estimator 27 | 28 | # Result Output 29 | print('Value of the European call option %5.3f.' % C0) 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Dr. Yves J. Hilpisch | The Python Quants GmbH | http://tpq.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifics 2 | *.cfg 3 | *.swp 4 | _build/ 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ -------------------------------------------------------------------------------- /files/code/why_python.txt: -------------------------------------------------------------------------------- 1 | # tag::MONTE_CARLO[] 2 | In [1]: import math 3 | import numpy as np # <1> 4 | 5 | In [2]: S0 = 100. # <2> 6 | K = 105. # <2> 7 | T = 1.0 # <2> 8 | r = 0.05 # <2> 9 | sigma = 0.2 # <2> 10 | 11 | In [3]: I = 100000 # <2> 12 | 13 | In [4]: np.random.seed(1000) # <3> 14 | 15 | In [5]: z = np.random.standard_normal(I) # <4> 16 | 17 | In [6]: ST = S0 * np.exp((r - sigma ** 2 / 2) * T + sigma * math.sqrt(T) * z) # <5> 18 | 19 | In [7]: hT = np.maximum(ST - K, 0) # <6> 20 | 21 | In [8]: C0 = math.exp(-r * T) * np.mean(hT) # <7> 22 | 23 | In [9]: print('Value of the European call option: {:5.3f}.'.format(C0)) # <8> 24 | Value of the European call option: 8.019. 25 | 26 | # end::MONTE_CARLO[] 27 | 28 | In [10]: %run bsm_mcs_euro.py 29 | Value of the European call option 7.989. 30 | 31 | # tag::SPX_EXAMPLE[] 32 | In [11]: import numpy as np # <1> 33 | import pandas as pd # <1> 34 | from pylab import plt, mpl # <2> 35 | 36 | In [12]: plt.style.use('seaborn') # <2> 37 | mpl.rcParams['font.family'] = 'serif' # <2> 38 | %matplotlib inline 39 | 40 | In [13]: data = pd.read_csv('http://hilpisch.com/tr_eikon_eod_data.csv', 41 | index_col=0, parse_dates=True) # <3> 42 | data = pd.DataFrame(data['.SPX']) # <4> 43 | data.dropna(inplace=True) # <4> 44 | data.info() # <5> 45 | 46 | DatetimeIndex: 2138 entries, 2010-01-04 to 2018-06-29 47 | Data columns (total 1 columns): 48 | .SPX 2138 non-null float64 49 | dtypes: float64(1) 50 | memory usage: 33.4 KB 51 | 52 | In [14]: data['rets'] = np.log(data / data.shift(1)) # <6> 53 | data['vola'] = data['rets'].rolling(252).std() * np.sqrt(252) # <7> 54 | 55 | In [15]: data[['.SPX', 'vola']].plot(subplots=True, figsize=(10, 6)); # <8> 56 | plt.savefig('../images/spx_volatility.png') 57 | # end::SPX_EXAMPLE[] 58 | 59 | # tag::PARADIGM_1[] 60 | In [16]: import math 61 | loops = 2500000 62 | a = range(1, loops) 63 | def f(x): 64 | return 3 * math.log(x) + math.cos(x) ** 2 65 | %timeit r = [f(x) for x in a] 66 | 1.21 s ± 8.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 67 | 68 | # end::PARADIGM_1[] 69 | 70 | # tag::PARADIGM_2[] 71 | In [17]: import numpy as np 72 | a = np.arange(1, loops) 73 | %timeit r = 3 * np.log(a) + np.cos(a) ** 2 74 | 94.7 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 75 | 76 | # end::PARADIGM_2[] 77 | 78 | # tag::PARADIGM_3[] 79 | In [18]: import numexpr as ne 80 | ne.set_num_threads(1) 81 | f = '3 * log(a) + cos(a) ** 2' 82 | %timeit r = ne.evaluate(f) 83 | 37.5 ms ± 936 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 84 | 85 | # end::PARADIGM_3[] 86 | 87 | # tag::PARADIGM_4[] 88 | In [19]: ne.set_num_threads(4) 89 | %timeit r = ne.evaluate(f) 90 | 12.8 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 91 | 92 | # end::PARADIGM_4[] 93 | 94 | -------------------------------------------------------------------------------- /files/code/why_python.txt~: -------------------------------------------------------------------------------- 1 | # tag::MONTE_CARLO[] 2 | In [1]: import math 3 | import numpy as np # <1> 4 | 5 | In [2]: S0 = 100. # <2> 6 | K = 105. # <2> 7 | T = 1.0 # <2> 8 | r = 0.05 # <2> 9 | sigma = 0.2 # <2> 10 | 11 | In [3]: I = 100000 # <2> 12 | 13 | In [4]: np.random.seed(1000) # <3> 14 | 15 | In [5]: z = np.random.standard_normal(I) # <4> 16 | 17 | In [6]: ST = S0 * np.exp((r - sigma ** 2 / 2) * T + sigma * math.sqrt(T) * z) # <5> 18 | 19 | In [7]: hT = np.maximum(ST - K, 0) # <6> 20 | 21 | In [8]: C0 = math.exp(-r * T) * np.mean(hT) # <7> 22 | 23 | In [9]: print('Value of the European call option: {:5.3f}.'.format(C0)) # <8> 24 | Value of the European call option: 8.019. 25 | 26 | # end::MONTE_CARLO[] 27 | 28 | In [10]: %run bsm_mcs_euro.py 29 | Value of the European call option 7.989. 30 | 31 | # tag::SPX_EXAMPLE[] 32 | In [11]: import numpy as np # <1> 33 | import pandas as pd # <1> 34 | from pylab import plt, mpl # <2> 35 | 36 | In [12]: plt.style.use('seaborn') # <2> 37 | mpl.rcParams['font.family'] = 'serif' # <2> 38 | %matplotlib inline 39 | 40 | In [13]: data = pd.read_csv('http://hilpisch.com/tr_eikon_eod_data.csv', 41 | index_col=0, parse_dates=True) # <3> 42 | data = pd.DataFrame(data['.SPX']) # <4> 43 | data.dropna(inplace=True) # <4> 44 | data.info() # <5> 45 | 46 | DatetimeIndex: 2138 entries, 2010-01-04 to 2018-06-29 47 | Data columns (total 1 columns): 48 | .SPX 2138 non-null float64 49 | dtypes: float64(1) 50 | memory usage: 33.4 KB 51 | 52 | In [14]: data['rets'] = np.log(data / data.shift(1)) # <6> 53 | data['vola'] = data['rets'].rolling(252).std() * np.sqrt(252) # <7> 54 | 55 | In [15]: data[['.SPX', 'vola']].plot(subplots=True, figsize=(10, 6)); # <8> 56 | plt.savefig('../images/spx_volatility.png') 57 | # end::SPX_EXAMPLE[] 58 | 59 | # tag::PARADIGM_1[] 60 | In [16]: import math 61 | loops = 2500000 62 | a = range(1, loops) 63 | def f(x): 64 | return 3 * math.log(x) + math.cos(x) ** 2 65 | %timeit r = [f(x) for x in a] 66 | 1.21 s ± 8.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 67 | 68 | # end::PARADIGM_1[] 69 | 70 | # tag::PARADIGM_2[] 71 | In [17]: import numpy as np 72 | a = np.arange(1, loops) 73 | %timeit r = 3 * np.log(a) + np.cos(a) ** 2 74 | 94.7 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 75 | 76 | # end::PARADIGM_2[] 77 | 78 | # tag::PARADIGM_3[] 79 | In [18]: import numexpr as ne 80 | ne.set_num_threads(1) 81 | f = '3 * log(a) + cos(a) ** 2' 82 | %timeit r = ne.evaluate(f) 83 | 37.5 ms ± 936 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 84 | 85 | # end::PARADIGM_3[] 86 | 87 | # tag::PARADIGM_4[] 88 | In [19]: ne.set_num_threads(4) 89 | %timeit r = ne.evaluate(f) 90 | 12.8 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 91 | 92 | # end::PARADIGM_4[] 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | The Python Quants
3 | 4 | # tpqad 5 | 6 | ## Asciidoctor with a Twist 7 | 8 | This repository provides a few (unpolished) helper scripts to write larger documents with Asciidoctor that import (Python) code from Jupyter Notebook semi-automatically. 9 | 10 | The approach introduced hase been used e.g. to write the book Python for Finance (2nd ed., O'Reilly). See http://py4fi.tpq.io. 11 | 12 | The repository is authored and maintained by The Python Quants GmbH. © Dr. Yves J. Hilpisch. MIT License. 13 | 14 | ## Requirements 15 | 16 | The following requires a standard Python 3.x installation as well as the installation of Asciidoctor — see https://asciidoctor.org. 17 | 18 | ## Basic Idea 19 | 20 | The basic idea of the approach is to use Asciidoctor as the basis. Codes can be written and stored in Jupyter Notebooks. Tags are added that later (after a parsing step) are used to include code snippets automatically into the Asciidoctor files. The same holds true for images/plots that are created during the execution of a Jupyter Notebook. 21 | 22 | ## Sources 23 | 24 | The files in the `files` folder are taken from earlier draft versions of **Python for Finance** (2nd ed., O'Reilly, http://py4fi.tpq.io). They have been modified for the simplified example to follow. 25 | 26 | ## Files and Steps 27 | 28 | To render the example files (see `files` folder) with Asciidoctor, do the following. 29 | 30 | **First**, execute the Jupyter Notebook (`code/why_python.ipynb`) and save it. Note the tags added in `raw` cells. Plots are saved automatically (in `images` folder). 31 | 32 | **Second**, on the shell in the `files` folder, parse the executed and saved Jupyter Notebook via 33 | 34 | python nb_parse.py code/why_python.ipynb 35 | 36 | This creates a text file with location and name `code/why_python.txt`. 37 | 38 | **Third**, on the shell, execute 39 | 40 | asciidoctor -a stem=latexmath py4fi.asciidoc 41 | 42 | to render a **HTML version**. 43 | 44 | **Fourth**, on the shell, execute 45 | 46 | asciidoctor-pdf -r asciidoctor-mathematical -a pdf-style=basic-theme.yml py4fi.asciidoc 47 | rm *.png 48 | 49 | to render a **PDF version** and delete temporary `png` files. 50 | 51 | ## `nb_parse.py` 52 | 53 | The file `nb_parse.py` is at the core of the example and the approach presented. It has grown over the course of writing **Python for Finance** (2nd ed.). However, it has not been reenginered or optimized in any way. Therefore, use it with caution and take it maybe as a starting point for your custom conversions. 54 | 55 | ## Example Output 56 | 57 | The following screenshot shows example output from the **HTML version**. 58 | 59 | 60 | 61 | The following screenshot shows example output from the **PDF version**. 62 | 63 | 64 | 65 | The Python Quants
66 | 67 | http://tpq.io | @dyjh | training@tpq.io 68 | -------------------------------------------------------------------------------- /tpqad.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "slideshow": { 7 | "slide_type": "slide" 8 | } 9 | }, 10 | "source": [ 11 | "\"The
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# tpqad" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Asciidoctor with a Twist" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "This repository provides a few (unpolished) helper scripts to write larger documents with Asciidoctor that import (Python) code from Jupyter Notebook semi-automatically.\n", 33 | "\n", 34 | "The approach introduced hase been used e.g. to write the book Python for Finance (2nd ed., O'Reilly). See http://py4fi.tpq.io.\n", 35 | "\n", 36 | "The repository is authored and maintained by The Python Quants GmbH. © Dr. Yves J. Hilpisch. MIT License." 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Requirements" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "The following requires a standard Python 3.x installation as well as the installation of Asciidoctor — see https://asciidoctor.org." 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Basic Idea" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "The basic idea of the approach is to use Asciidoctor as the basis. Codes can be written and stored in Jupyter Notebooks. Tags are added that later (after a parsing step) are used to include code snippets automatically into the Asciidoctor files. The same holds true for images/plots that are created during the execution of a Jupyter Notebook." 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "## Sources" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "The files in the `files` folder are taken from earlier draft versions of **Python for Finance** (2nd ed., O'Reilly, http://py4fi.tpq.io). They have been modified for the simplified example to follow." 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "## Files and Steps" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "To render the example files (see `files` folder) with Asciidoctor, do the following.\n", 93 | "\n", 94 | "**First**, execute the Jupyter Notebook (`code/why_python.ipynb`) and save it. Note the tags added in `raw` cells. Plots are saved automatically (in `images` folder).\n", 95 | "\n", 96 | "**Second**, on the shell in the `files` folder, parse the executed and saved Jupyter Notebook via\n", 97 | "\n", 98 | " python nb_parse.py code/why_python.ipynb\n", 99 | " \n", 100 | "This creates a text file with location and name `code/why_python.txt`.\n", 101 | "\n", 102 | "**Third**, on the shell, execute\n", 103 | "\n", 104 | " asciidoctor -a stem=latexmath py4fi.asciidoc\n", 105 | " \n", 106 | "to render a **HTML version**.\n", 107 | "\n", 108 | "**Fourth**, on the shell, execute\n", 109 | "\n", 110 | " asciidoctor-pdf -r asciidoctor-mathematical -a pdf-style=basic-theme.yml py4fi.asciidoc\n", 111 | " rm *.png\n", 112 | " \n", 113 | "to render a **PDF version** and delete temporary `png` files." 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "## `nb_parse.py`" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "The file `nb_parse.py` is at the core of the example and the approach presented. It has grown over the course of writing **Python for Finance** (2nd ed.). However, it has not been reenginered or optimized in any way. Therefore, use it with caution and take it maybe as a starting point for your custom conversions." 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "## Example Output" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "The following screenshot shows example output from the **HTML version**.\n", 142 | "\n", 143 | "" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "The following screenshot shows example output from the **PDF version**.\n", 151 | "\n", 152 | "" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": { 158 | "slideshow": { 159 | "slide_type": "slide" 160 | } 161 | }, 162 | "source": [ 163 | "\"The
\n", 164 | "\n", 165 | "http://tpq.io | @dyjh | training@tpq.io" 166 | ] 167 | } 168 | ], 169 | "metadata": { 170 | "anaconda-cloud": {}, 171 | "kernelspec": { 172 | "display_name": "Python 3", 173 | "language": "python", 174 | "name": "python3" 175 | }, 176 | "language_info": { 177 | "codemirror_mode": { 178 | "name": "ipython", 179 | "version": 3 180 | }, 181 | "file_extension": ".py", 182 | "mimetype": "text/x-python", 183 | "name": "python", 184 | "nbconvert_exporter": "python", 185 | "pygments_lexer": "ipython3", 186 | "version": "3.6.7" 187 | } 188 | }, 189 | "nbformat": 4, 190 | "nbformat_minor": 1 191 | } 192 | -------------------------------------------------------------------------------- /files/nb_parse.py: -------------------------------------------------------------------------------- 1 | # 2 | # Script to Convert Jupyter Notebook 3 | # to IPython Style Text File 4 | # 5 | # (c) The Python Quants GmbH 6 | # WORK IN PROGRESS | DRAFT VERSION 7 | # FOR ILLUSTRATION PURPOSES ONLY 8 | import re 9 | import os 10 | import sys 11 | import json 12 | 13 | ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') 14 | 15 | 16 | def parse(argv): 17 | if len(argv) < 1: 18 | raise ValueError('Please provide a file') 19 | path = argv[0] 20 | 21 | if not os.path.isfile(path): 22 | raise ValueError('Can not open %s. No such file or directory.' % path) 23 | 24 | parts = path.split('.') 25 | if len(parts) < 2 or len(parts) > 2 or parts[1] != 'ipynb': 26 | raise ValueError('Invalid filename') 27 | target = parts[0] + '.' + 'txt' 28 | 29 | notebook = read_in(argv[0]) 30 | 31 | with open(target, 'w') as target_file: 32 | for cell in notebook['cells']: 33 | if cell['cell_type'] == 'raw': 34 | for l in cell['source']: 35 | target_file.write(l) 36 | if l.startswith('# end'): 37 | target_file.write('\n\n') 38 | else: 39 | target_file.write('\n') 40 | 41 | if cell['cell_type'] == 'code': 42 | if 'execution_count' in cell: 43 | execution_count = cell['execution_count'] 44 | else: 45 | execution_count = "" 46 | prefix = 'In [%s]: ' % execution_count 47 | len_prefix = len(prefix) 48 | code = '' 49 | first = 0 50 | for line in cell['source']: 51 | code = code + ' ' * len_prefix * first + line 52 | first = 1 53 | text_input = prefix + code 54 | target_file.write(text_input) 55 | if cell['outputs']: 56 | target_file.write('\n') 57 | else: 58 | target_file.write('\n\n') 59 | 60 | for output in cell['outputs']: 61 | if 'execution_count' in output: 62 | execution_count = output['execution_count'] 63 | else: 64 | execution_count = '' 65 | suffix = 'Out[%s]: ' % execution_count 66 | len_suffix = len(suffix) 67 | if 'data' in output and 'text/plain' in output['data'] \ 68 | and 'image/png' not in output['data']: 69 | result = "" 70 | first = 0 71 | lb = 0 72 | for line in output['data']['text/plain']: 73 | add_lines = list() 74 | if len(line) + len_suffix <= 81: 75 | add_lines.append(line) 76 | else: 77 | add_lines = break_long_lines(line, len_suffix) 78 | for l in add_lines: 79 | if len(add_lines) >= 2: 80 | lb = 1 81 | result += (' ' * len_suffix * first + l + 82 | lb * '\n') 83 | first = 1 84 | if len(add_lines) == 2: 85 | result = result[:-1] 86 | text_output = suffix + result 87 | target_file.write(text_output) 88 | target_file.write('\n\n') 89 | 90 | if 'name' in output: 91 | result = "" 92 | first = 0 93 | result = "" 94 | first = 0 95 | lb = 0 96 | for line in output['text']: 97 | add_lines = list() 98 | if len(line) + len_suffix <= 81: 99 | add_lines.append(line) 100 | else: 101 | add_lines = break_long_lines(line, len_suffix) 102 | lb = 1 103 | for l in add_lines: 104 | result += ' ' * (len_prefix + 0) * \ 105 | first + l + lb * '\n' 106 | first = 1 107 | if len(add_lines) == 2: 108 | result = result[:-1] 109 | lb = 0 110 | 111 | text_output = (len_prefix + 0) * ' ' + result 112 | target_file.write(text_output) 113 | target_file.write('\n') 114 | 115 | if 'ename' in output: 116 | result = "" 117 | first = 0 118 | target_file.write('\n') 119 | for line in output['traceback']: 120 | line = ansi_escape.sub('', line) 121 | result = (result + ' ' * len_prefix * first + 122 | line + '\n') 123 | first = 1 124 | text_output = len_prefix * ' ' + result 125 | target_file.write(text_output) 126 | target_file.write('\n') 127 | 128 | 129 | def break_long_lines(line, len_suffix): 130 | comma = False 131 | # line.replace('\n', '') 132 | if line[len_suffix + 1:].find(' ') >= 0: 133 | line_parts = line.split(' ') 134 | elif line[len_suffix + 1:].find(',') >= 0: 135 | line_parts = line.split(',') 136 | comma = True 137 | else: 138 | line_parts = [line[start:start + 70] for 139 | start in range(0, len(line) + 1, 70)] 140 | p_line = '' 141 | lines = list() 142 | splitter = ',' if comma else ' ' 143 | for l in line_parts: 144 | if len(p_line) + len(l) + len_suffix < 81: 145 | if len(p_line) == 0: 146 | p_line = l 147 | else: 148 | p_line = p_line + splitter + l 149 | else: 150 | lines.append(p_line + splitter) 151 | p_line = splitter + l 152 | lines.append(p_line) 153 | return lines 154 | 155 | 156 | def read_in(path): 157 | with open(path, 'r') as f: 158 | notebook = f.read() 159 | 160 | notebook = json.loads(notebook) 161 | return notebook 162 | 163 | 164 | if __name__ == '__main__': 165 | parse(sys.argv[1:]) 166 | import os 167 | fn = sys.argv[1].split('.')[0] + '.txt' 168 | os.rename(fn, fn + '~') 169 | with open(fn + '~', 'r') as f: 170 | with open(fn, 'w') as o: 171 | for line in f: 172 | if line.find('# plt.save') >= 0: 173 | pass 174 | else: 175 | o.write(line) 176 | # os.remove(sys.argv[2] + '~') 177 | -------------------------------------------------------------------------------- /files/basic-theme.yml: -------------------------------------------------------------------------------- 1 | font: 2 | catalog: 3 | # Noto Serif supports Latin, Latin-1 Supplement, Latin Extended-A, Greek, Cyrillic, Vietnamese & an assortment of symbols 4 | Noto Serif: 5 | normal: notoserif-regular-subset.ttf 6 | bold: notoserif-bold-subset.ttf 7 | italic: notoserif-italic-subset.ttf 8 | bold_italic: notoserif-bold_italic-subset.ttf 9 | # M+ 1mn supports ASCII and the circled numbers used for conums 10 | M+ 1mn: 11 | normal: mplus1mn-regular-ascii-conums.ttf 12 | bold: mplus1mn-bold-ascii.ttf 13 | italic: mplus1mn-italic-ascii.ttf 14 | bold_italic: mplus1mn-bold_italic-ascii.ttf 15 | # M+ 1p supports Latin, Latin-1 Supplement, Latin Extended, Greek, Cyrillic, Vietnamese, Japanese & an assortment of symbols 16 | # It also provides arrows for ->, <-, => and <= replacements in case these glyphs are missing from font 17 | M+ 1p Fallback: 18 | normal: mplus1p-regular-fallback.ttf 19 | bold: mplus1p-regular-fallback.ttf 20 | italic: mplus1p-regular-fallback.ttf 21 | bold_italic: mplus1p-regular-fallback.ttf 22 | fallbacks: 23 | - M+ 1p Fallback 24 | page: 25 | background_color: ffffff 26 | layout: portrait 27 | margin: [0.5in, 0.67in, 0.67in, 0.67in] 28 | # margin_inner and margin_outer keys are used for recto/verso print margins when media=press 29 | margin_inner: 0.75in 30 | margin_outer: 0.59in 31 | size: A4 32 | base: 33 | align: justify 34 | # color as hex string (leading # is optional) 35 | font_color: 333333 36 | # color as RGB array 37 | #font_color: [51, 51, 51] 38 | # color as CMYK array (approximated) 39 | #font_color: [0, 0, 0, 0.92] 40 | #font_color: [0, 0, 0, 92%] 41 | font_family: Noto Serif 42 | # choose one of these font_size/line_height_length combinations 43 | #font_size: 14 44 | #line_height_length: 20 45 | #font_size: 11.25 46 | #line_height_length: 18 47 | #font_size: 11.2 48 | #line_height_length: 16 49 | font_size: 12 50 | #line_height_length: 15 51 | # correct line height for Noto Serif metrics 52 | line_height_length: 11 53 | #font_size: 11.25 54 | #line_height_length: 18 55 | line_height: $base_line_height_length / $base_font_size 56 | font_size_large: round($base_font_size * 1.25) 57 | font_size_small: round($base_font_size * 0.85) 58 | font_size_min: $base_font_size * 0.75 59 | font_style: normal 60 | border_color: eeeeee 61 | border_radius: 4 62 | border_width: 0.5 63 | # FIXME vertical_rhythm is weird; we should think in terms of ems 64 | #vertical_rhythm: $base_line_height_length * 2 / 3 65 | # correct line height for Noto Serif metrics (comes with built-in line height) 66 | vertical_rhythm: $base_line_height_length 67 | horizontal_rhythm: $base_line_height_length 68 | # QUESTION should vertical_spacing be block_spacing instead? 69 | vertical_spacing: $vertical_rhythm 70 | link: 71 | font_color: 428bca 72 | # literal is currently used for inline monospaced in prose and table cells 73 | literal: 74 | font_color: b12146 75 | font_family: M+ 1mn 76 | menu_caret_content: " \u203a " 77 | heading: 78 | align: left 79 | #font_color: 181818 80 | font_color: $base_font_color 81 | font_family: $base_font_family 82 | font_style: bold 83 | # h1 is used for part titles (book doctype only) 84 | h1_font_size: floor($base_font_size * 2.6) 85 | # h2 is used for chapter titles (book doctype only) 86 | h2_font_size: floor($base_font_size * 2.15) 87 | h3_font_size: round($base_font_size * 1.7) 88 | h4_font_size: $base_font_size_large 89 | h5_font_size: $base_font_size 90 | h6_font_size: $base_font_size_small 91 | #line_height: 1.4 92 | # correct line height for Noto Serif metrics (comes with built-in line height) 93 | line_height: 1 94 | margin_top: $vertical_rhythm * 0.4 95 | margin_bottom: $vertical_rhythm * 0.9 96 | title_page: 97 | align: right 98 | logo: 99 | top: 10% 100 | title: 101 | top: 55% 102 | font_size: $heading_h1_font_size 103 | font_color: 999999 104 | line_height: 0.9 105 | subtitle: 106 | font_size: $heading_h3_font_size 107 | font_style: bold_italic 108 | line_height: 1 109 | authors: 110 | margin_top: $base_font_size * 1.25 111 | font_size: $base_font_size_large 112 | font_color: 181818 113 | revision: 114 | margin_top: $base_font_size * 1.25 115 | block: 116 | margin_top: 0 117 | margin_bottom: $vertical_rhythm 118 | caption: 119 | align: left 120 | font_size: $base_font_size * 0.95 121 | font_style: italic 122 | # FIXME perhaps set line_height instead of / in addition to margins? 123 | margin_inside: $vertical_rhythm / 3 124 | #margin_inside: $vertical_rhythm / 4 125 | margin_outside: 0 126 | lead: 127 | font_size: $base_font_size_large 128 | line_height: 1.4 129 | abstract: 130 | font_color: 5c6266 131 | font_size: $lead_font_size 132 | line_height: $lead_line_height 133 | font_style: italic 134 | first_line_font_style: bold 135 | title: 136 | align: center 137 | font_color: $heading_font_color 138 | font_family: $heading_font_family 139 | font_size: $heading_h4_font_size 140 | font_style: $heading_font_style 141 | admonition: 142 | column_rule_color: $base_border_color 143 | column_rule_width: $base_border_width 144 | padding: [0, $horizontal_rhythm, 0, $horizontal_rhythm] 145 | #icon: 146 | # tip: 147 | # name: fa-lightbulb-o 148 | # stroke_color: 111111 149 | # size: 24 150 | label: 151 | text_transform: uppercase 152 | font_style: bold 153 | blockquote: 154 | font_color: $base_font_color 155 | font_size: $base_font_size - 1 156 | line_height: $base_line_height - 2 157 | border_color: $base_border_color 158 | border_width: 5 159 | # FIXME disable negative padding bottom once margin collapsing is implemented 160 | padding: [0, $horizontal_rhythm, $block_margin_bottom * -0.75, $horizontal_rhythm + $blockquote_border_width / 2] 161 | cite_font_size: $base_font_size_small 162 | cite_font_color: 999999 163 | # code is used for source blocks (perhaps change to source or listing?) 164 | code: 165 | font_color: $base_font_color 166 | font_family: $literal_font_family 167 | font_size: $base_font_size - 1 # ceil($base_font_size) 168 | padding: $code_font_size 169 | line_height: 1.25 170 | # line_gap is an experimental property to control how a background color is applied to an inline block element 171 | line_gap: 3.8 172 | background_color: f5f5f5 173 | border_color: cccccc 174 | border_radius: $base_border_radius 175 | border_width: 0.75 176 | conum: 177 | font_family: M+ 1mn 178 | font_color: $literal_font_color 179 | font_size: $base_font_size 180 | line_height: 4 / 3 181 | example: 182 | border_color: $base_border_color 183 | border_radius: $base_border_radius 184 | border_width: 0.75 185 | background_color: transparent 186 | # FIXME reenable padding bottom once margin collapsing is implemented 187 | padding: [$vertical_rhythm, $horizontal_rhythm, 0, $horizontal_rhythm] 188 | image: 189 | align: left 190 | prose: 191 | margin_top: $block_margin_top 192 | margin_bottom: $block_margin_bottom 193 | sidebar: 194 | border_color: $page_background_color 195 | border_radius: $base_border_radius 196 | border_width: $base_border_width 197 | background_color: eeeeee 198 | # FIXME reenable padding bottom once margin collapsing is implemented 199 | padding: [$vertical_rhythm, $vertical_rhythm * 1.25, 0, $vertical_rhythm * 1.25] 200 | title: 201 | align: center 202 | font_color: $heading_font_color 203 | font_family: $heading_font_family 204 | font_size: $heading_h4_font_size 205 | font_style: $heading_font_style 206 | thematic_break: 207 | border_color: $base_border_color 208 | border_style: solid 209 | border_width: $base_border_width 210 | margin_top: $vertical_rhythm * 0.5 211 | margin_bottom: $vertical_rhythm * 1.5 212 | description_list: 213 | term_font_style: italic 214 | term_spacing: $vertical_rhythm / 4 215 | description_indent: $horizontal_rhythm * 1.25 216 | outline_list: 217 | indent: $horizontal_rhythm * 1.5 218 | #marker_font_color: 404040 219 | # NOTE outline_list_item_spacing applies to list items that do not have complex content 220 | item_spacing: $vertical_rhythm / 2 221 | table: 222 | background_color: $page_background_color 223 | #head_background_color: 224 | #head_font_color: $base_font_color 225 | head_font_style: bold 226 | even_row_background_color: f9f9f9 227 | #odd_row_background_color: 228 | foot_background_color: f0f0f0 229 | border_color: dddddd 230 | border_width: $base_border_width 231 | cell_padding: 3 232 | toc: 233 | indent: $horizontal_rhythm 234 | line_height: 1.4 235 | dot_leader: 236 | #content: ". " 237 | font_color: a9a9a9 238 | #levels: 2 3 239 | # NOTE in addition to footer, header is also supported 240 | footer: 241 | font_size: $base_font_size_small 242 | # NOTE if background_color is set, background and border will span width of page 243 | border_color: dddddd 244 | border_width: 0.25 245 | height: $base_line_height_length * 2.5 246 | line_height: 1 247 | padding: [$base_line_height_length / 2, 1, 0, 1] 248 | vertical_align: top 249 | #image_vertical_align: or 250 | # additional attributes for content: 251 | # * {page-count} 252 | # * {page-number} 253 | # * {document-title} 254 | # * {document-subtitle} 255 | # * {chapter-title} 256 | # * {section-title} 257 | # * {section-or-chapter-title} 258 | recto: 259 | #columns: "<50% =0% >50%" 260 | right: 261 | content: '{page-number}' 262 | #content: '{section-or-chapter-title} | {page-number}' 263 | #content: '{document-title} | {page-number}' 264 | #center: 265 | # content: '{page-number}' 266 | verso: 267 | #columns: $footer_recto_columns 268 | left: 269 | content: $footer_recto_right_content 270 | #content: '{page-number} | {chapter-title}' 271 | #center: 272 | # content: '{page-number}' -------------------------------------------------------------------------------- /files/why_python.asciidoc: -------------------------------------------------------------------------------- 1 | 2 | [[why_python_for_finance]] 3 | == Why Python for Finance 4 | 5 | [role="align_me_right"] 6 | [quote, Hugo Banziger] 7 | ____ 8 | Banks are essentially technology firms. 9 | ____ 10 | 11 | 12 | 13 | === Finance and Python Syntax 14 | 15 | Most people who make their first steps with Python in a finance context may attack an algorithmic problem. This is similar to a scientist who, for example, wants to solve a differential equation, wants to evaluate an integral or simply wants to visualize some data. In general, at this stage, there is only little thought spent on topics like a formal development process, testing, documentation or deployment. However, this especially seems to be the stage when people fall in love with Python. A major reason for this might be that Python syntax is generally quite close to the mathematical syntax used to describe scientific problems or financial algorithms. 16 | 17 | This aspect can be illustrated by a financial algorithm, namely the valuation of a European call option by Monte Carlo simulation. The example considers a Black-Scholes-Merton (BSM) setup in which the option's underlying risk factor follows a geometric Brownian motion. 18 | 19 | Assume the following numerical _parameter values_ for the valuation: 20 | 21 | * initial stock index level latexmath:[S_0 = 100] 22 | * strike price of the European call option latexmath:[K = 105] 23 | * time-to-maturity latexmath:[T = 1] year 24 | * constant, riskless short rate latexmath:[r = 0.05] 25 | * constant volatility latexmath:[\sigma = 0.2] 26 | 27 | 28 | In the BSM model, the index level at maturity is a random variable, given by <> with latexmath:[z] being a standard normally distributed random variable. 29 | 30 | [[bsm_rv]] 31 | [latexmath] 32 | .Black-Scholes-Merton (1973) index level at maturity 33 | ++++ 34 | \begin{equation} 35 | S_T = S_0 \exp \left( \left( r - \frac{1}{2} \sigma^2 \right) T + \sigma \sqrt{T} z \right) 36 | \end{equation} 37 | ++++ 38 | 39 | The following is an _algorithmic description_ of the Monte Carlo valuation procedure: 40 | 41 | . draw latexmath:[I] pseudo-random numbers latexmath:[z(i), i \in \{1, 2, ..., I \}], from the standard normal distribution 42 | . calculate all resulting index levels at maturity latexmath:[S_T(i)] for given latexmath:[z(i)] and <> 43 | . calculate all inner values of the option at maturity as latexmath:[h_T(i)= \max \left(S_T(i) - K, 0 \right)] 44 | . estimate the option present value via the Monte Carlo estimator as given in <> 45 | 46 | [[bsm_mcs_est]] 47 | [latexmath] 48 | .Monte Carlo estimator for European option 49 | ++++ 50 | \begin{equation} 51 | C_0 \approx e^{-rT} \frac{1}{I} \sum_I h_T(i) 52 | \end{equation} 53 | ++++ 54 | 55 | This problem and algorithm is now to be translated into Python code. The code below implements the required steps. 56 | 57 | [source,python] 58 | ---- 59 | include::code/why_python.txt[tags=MONTE_CARLO] 60 | ---- 61 | <1> The model and simulation parameter values are defined. 62 | <2> `NumPy` is used here as the main package. 63 | <3> The seed value for the random number generator is fixed. 64 | <4> Standard normally distributed random numbers are drawn. 65 | <5> End-of-period values are simulated. 66 | <6> The option payoffs at maturity are calculated. 67 | <7> The Monte Carlo estimator is evaluated. 68 | <8> The resulting value estimate is printed. 69 | 70 | Three aspects are worth highlighting: 71 | 72 | syntax:: 73 | the Python syntax is indeed quite close to the mathematical syntax, e.g., when it comes to the parameter value assignments 74 | translation:: 75 | every mathematical and/or algorithmic statement can generally be translated into a _single_ line of Python code 76 | vectorization:: 77 | one of the strengths of `NumPy` is the compact, vectorized syntax, e.g., allowing for 100,000 calculations within a single line of code 78 | 79 | This code can be used in an interactive environment like `IPython` or `Jupyter Notebook`. However, code that is meant to be reused regularly typically gets organized in so-called _modules_ (or _scripts_), which are single Python files (technically text files) with the suffix `.py`. Such a module could in this case look like <> and could be saved as a file named `bsm_mcs_euro.py`. 80 | 81 | 82 | [[bsm_mcs_euro]] 83 | .Monte Carlo valuation of European call option 84 | ==== 85 | [source, python] 86 | ---- 87 | include::code/bsm_mcs_euro.py[] 88 | ---- 89 | ==== 90 | 91 | The algorithmic example in this subsection illustrates that Python, with its very syntax, is well suited to complement the classic duo of scientific languages, English and mathematics. It seems that adding `Python` to the set of scientific languages makes it more well rounded. One then has: 92 | 93 | * *English* for _writing, talking_ about scientific and financial problems, etc. 94 | * *Mathematics* for _concisely, exactly describing and modeling_ abstract aspects, algorithms, complex quantities, etc. 95 | * *Python* for _technically modeling and implementing_ abstract aspects, algorithms, complex quantities, etc. 96 | 97 | .Mathematics and Python Syntax 98 | [TIP] 99 | ==== 100 | There is hardly any programming language that comes as close to mathematical syntax as Python. Numerical algorithms are therefore in general straightforward to translate from the mathematical representation into the `Pythonic` implementation. This makes prototyping, development and code maintenance in finance quite efficient with Python. 101 | ==== 102 | 103 | In some areas, it is common practice to use _pseudo-code_ and therewith to introduce a fourth language family member. The role of pseudo-code is to represent, for example, financial algorithms in a more technical fashion that is both still close to the mathematical representation and already quite close to the technical implementation. In addition to the algorithm itself, pseudo-code takes into account how computers work in principle. 104 | 105 | This practice generally has its cause in the fact that with most (compiled) programming languages the technical implementation is quite "`far away`" from its formal, mathematical representation. The majority of programming languages make it necessary to include so many elements that are only technically required that it is hard to see the equivalence between the mathematics and the code. 106 | 107 | Nowadays, Python is often used in a _pseudo-code way_ since its syntax is almost analogous to the mathematics and since the technical "`overhead`" is kept to a minimum. This is accomplished by a number of high-level concepts embodied in the language that not only have their advantages but also come in general with risks and/or other costs. However, it is safe to say that with Python you can, whenever the need arises, follow the same strict implementation and coding practices that other languages might require from the outset. In that sense, Python can provide the best of both worlds: _high-level abstraction_ and _rigorous implementation_. 108 | 109 | 110 | === Efficiency and Productivity Through Python 111 | 112 | At a high level, benefits from using Python can be measured in three dimensions: 113 | 114 | efficiency:: 115 | how can Python help in getting results faster, in saving costs, and in saving time? 116 | productivity:: 117 | how can Python help in getting more done with the same resources (people, assets, etc.)? 118 | quality:: 119 | what does Python allow to do that alternative technologies do not allow for? 120 | 121 | A discussion of these aspects can by nature not be exhaustive. However, it can highlight some arguments as a starting point. 122 | 123 | ==== Shorter Time-to-Results 124 | 125 | A field where the efficiency of Python becomes quite obvious is interactive data analytics. This is a field that benefits tremendously from such powerful tools as `IPython`, `Jupyter Notebook` and packages like `pandas`. 126 | 127 | Consider a finance student, writing her master's thesis and interested in S&P 500 index values. She wants to analyze historical index levels for, say, a few years to see how the volatility of the index has fluctuated over time. She wants to find evidence that volatility, in contrast to some typical model assumptions, fluctuates over time and is far from being constant. The results should also be visualized. She mainly has to do the following: 128 | 129 | * retrieve index level data from the web 130 | * calculate the annualized rolling standard deviation of the log returns (volatility) 131 | * plot the index level data and the volatility results 132 | 133 | These tasks are complex enough that not too long ago one would have considered them to be something for professional financial analysts only. Today, even the finance student can easily cope with such problems. The code below shows how exactly this works -- without worrying about syntax details at this stage (everything is explained in detail in subsequent chapters). 134 | 135 | [source,python] 136 | ---- 137 | include::code/why_python.txt[tags=SPX_EXAMPLE] 138 | ---- 139 | <1> This imports `NumPy` and `pandas`. 140 | <2> This imports `matplotlib` and configures the plotting style and approach for `Jupyter`. 141 | <3> `pd.read_csv()` allows the retrieval of remotely or locally stored data sets in `CSV` form. 142 | <4> A sub-set of the data is picked and `NaN` ("not a number") values eliminated. 143 | <5> This shows some meta-information about the data set. 144 | <6> The log returns are calculated in vectorized fashion ("`no looping`" on the Python level). 145 | <7> The rolling, annualized volatility is derived. 146 | <8> This finally plots the two time series. 147 | 148 | <> shows the graphical result of this brief interactive session. It can be considered almost amazing that a few lines of code suffice to implement three rather complex tasks typically encountered in financial analytics: data gathering, complex and repeated mathematical calculations as well as visualization of the results. The example illustrates that `pandas` makes working with whole time series almost as simple as doing mathematical operations on floating-point numbers. 149 | 150 | [[spx_vola]] 151 | .S&P 500 closing values and annualized volatility 152 | image::images/spx_volatility.png[] 153 | 154 | Translated to a professional finance context, the example implies that financial analysts can -- when applying the right Python tools and packages that provide high-level abstractions -- focus on their very domain and not on the technical intrinsicalities. Analysts can react faster, providing valuable insights almost in real-time and making sure they are one step ahead of the competition. This example of _increased efficiency_ can easily translate into measurable bottom-line effects. 155 | 156 | ==== Ensuring High Performance 157 | 158 | In general, it is accepted that Python has a rather concise syntax and that it is relatively efficient to code with. However, due to the very nature of Python being an interpreted language, the _prejudice_ persists that Python often is too slow for compute-intensive tasks in finance. Indeed, depending on the specific implementation approach, Python can be really slow. But it _does not have to be slow_ -- it can be highly performing in almost any application area. In principle, one can distinguish at least three different strategies for better performance: 159 | 160 | idioms and paradigms:: 161 | in general, many different ways can lead to the same result in Python, but sometimes with rather different performance characteristics; "`simply`" choosing the right way (e.g., a specific implementation approach, such as the judicious use of data structures, avoiding loops through vectorization or the use of a specific package such as `pandas`) can improve results significantly 162 | compiling:: 163 | nowadays, there are several performance packages available that provide compiled versions of important functions or that compile Python code statically or dynamically (at runtime or call time) to machine code, which can make such functions orders of magnitude faster than pure Python code; popular ones are `Cython` and `Numba` 164 | parallelization:: 165 | many computational tasks, in particular in finance, can significantly benefit from parallel execution; this is nothing special to Python but something that can easily be accomplished with it 166 | 167 | [TIP] 168 | .Performance Computing with Python 169 | ==== 170 | Python per se is not a high-performance computing technology. However, Python has developed into an ideal platform to access current performance technologies. In that sense, Python has become something like a _glue language_ for performance computing technologies. 171 | ==== 172 | 173 | Later chapters illustrate all three strategies in detail. This sub-section sticks to a simple, but still realistic, example that touches upon all three strategies. A quite common task in financial analytics is to evaluate complex mathematical expressions on large arrays of numbers. To this end, Python itself provides everything needed. 174 | 175 | [source,python] 176 | ---- 177 | include::code/why_python.txt[tags=PARADIGM_1] 178 | ---- 179 | 180 | The Python interpreter needs about 1.4 seconds in this case to evaluate the function `f` 2,500,000 times. The same task can be implemented using `NumPy`, which provides optimized (i.e., _pre-compiled_), functions to handle such array-based operations. 181 | 182 | [source,python] 183 | ---- 184 | include::code/why_python.txt[tags=PARADIGM_2] 185 | ---- 186 | 187 | Using `NumPy` considerably reduces the execution time to about 80 milliseconds. However, there is even a package specifically dedicated to this kind of task. It is called `numexpr`, for "`numerical expressions." It _compiles_ the expression to improve upon the performance of the general `NumPy` functionality by, for example, avoiding in-memory copies of `ndarray` objects along the way. 188 | 189 | [source,python] 190 | ---- 191 | include::code/why_python.txt[tags=PARADIGM_3] 192 | ---- 193 | 194 | Using this more specialized approach further reduces execution time to about 40 milliseconds. However, `numexpr` also has built-in capabilities to parallelize the execution of the respective operation. This allows us to use multiple threads of a CPU. 195 | 196 | [source,python] 197 | ---- 198 | include::code/why_python.txt[tags=PARADIGM_4] 199 | ---- 200 | 201 | Parallelization brings execution time further down to below 15 milliseconds in this case, with four threads utilized. Overall, this is a performance improvement of more than 90 times. Note, in particular, that this kind of improvement is possible without altering the basic problem/algorithm and without knowing any detail about compiling or parallelization approaches. The capabilities are accessible from a high level even by non-experts. However, one has to be aware, of course, of which capabilities and options exist. 202 | 203 | The example shows that Python provides a number of options to make more out of existing resources -- i.e., to _increase productivity_. With the parallel approach, three times as many calculations can be accomplished in the same amount of time as compared to the sequential approach -- in this case simply by telling Python to use multiple available CPU threads instead of just one. 204 | 205 | 206 | === Conclusions 207 | 208 | Python as a language -- and even more so as an ecosystem -- is an ideal technological framework for the financial industry as whole and the individual working in finance alike. It is characterized by a number of benefits, like an elegant syntax, efficient development approaches and usability for prototyping _as well as_ production. With its huge amount of available packages, libraries and tools, Python seems to have answers to most questions raised by recent developments in the financial industry in terms of analytics, data volumes and frequency, compliance and regulation, as well as technology itself. It has the potential to provide a _single, powerful, consistent framework_ with which to streamline end-to-end development and production efforts even across larger financial institutions. 209 | 210 | In addition, Python has become the programming language of choice for _artificial intelligence_ in general and _machine and deep learning_ in particular. Python is therefore both the right language for _data-driven finance_ as well as for _AI-first finance_, two recent trends that are about to reshape finance and the financial industry in fundamental ways. 211 | 212 | 213 | === Further Resources 214 | 215 | The following books cover several aspects only touched upon in this chapter in more detail (e.g. Python tools, derivatives analytics, machine learning in general, machine learning in finance): 216 | 217 | * Hilpisch, Yves (2015): _Derivatives Analytics with Python_. Wiley Finance, Chichester, England. http://dawp.tpq.io[]. 218 | * López de Prado, Marcos (2018): _Advances in Financial Machine Learning_. John Wiley & Sons, Hoboken. 219 | * VanderPlas, Jake (2016): _Python Data Science Handbook_. O'Reilly, Beijing et al. 220 | 221 | When it comes to algorithmic trading, the author's company offers a range of online training programs that focus on Python and other tools and techniques required in this rapidly growing field: 222 | 223 | * http://pyalgo.tpq.io[] -- online training course 224 | * http://certificate.tpq.io[] -- online training program 225 | 226 | Sources referenced in this chapter are, among others, the following: 227 | 228 | * Ding, Cubillas (2010): "`Optimizing the OTC Pricing and Valuation Infrastructure.`" _Celent study_. 229 | * Lewis, Michael (2014): _Flash Boys_. W. W. Norton & Company, New York. 230 | * Patterson, Scott (2010): _The Quants._ Crown Business, New York. 231 | -------------------------------------------------------------------------------- /files/code/why_python.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"The
" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "# Why Python for Finance?" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Finance and Python Syntax" 22 | ] 23 | }, 24 | { 25 | "cell_type": "raw", 26 | "metadata": {}, 27 | "source": [ 28 | "# tag::MONTE_CARLO[]" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 1, 34 | "metadata": { 35 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "import math\n", 40 | "import numpy as np # <1>" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": { 47 | "uuid": "1447b7bb-ed26-4c0f-9e0f-222dfd5d0c9b" 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "S0 = 100. # <2>\n", 52 | "K = 105. # <2>\n", 53 | "T = 1.0 # <2>\n", 54 | "r = 0.05 # <2>\n", 55 | "sigma = 0.2 # <2>" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "metadata": { 62 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 63 | }, 64 | "outputs": [], 65 | "source": [ 66 | "I = 100000 # <2>" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 4, 72 | "metadata": { 73 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 74 | }, 75 | "outputs": [], 76 | "source": [ 77 | "np.random.seed(1000) # <3>" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 5, 83 | "metadata": { 84 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "z = np.random.standard_normal(I) # <4>" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 6, 94 | "metadata": { 95 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 96 | }, 97 | "outputs": [], 98 | "source": [ 99 | "ST = S0 * np.exp((r - sigma ** 2 / 2) * T + sigma * math.sqrt(T) * z) # <5>" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 7, 105 | "metadata": { 106 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 107 | }, 108 | "outputs": [], 109 | "source": [ 110 | "hT = np.maximum(ST - K, 0) # <6>" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 8, 116 | "metadata": { 117 | "uuid": "a95c7301-39f7-4d51-937a-334f051c2d9e" 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "C0 = math.exp(-r * T) * np.mean(hT) # <7>" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 9, 127 | "metadata": { 128 | "uuid": "84aab05d-40de-4ef1-b3a8-eb08f86e1662" 129 | }, 130 | "outputs": [ 131 | { 132 | "name": "stdout", 133 | "output_type": "stream", 134 | "text": [ 135 | "Value of the European call option: 8.019.\n" 136 | ] 137 | } 138 | ], 139 | "source": [ 140 | "print('Value of the European call option: {:5.3f}.'.format(C0)) # <8>" 141 | ] 142 | }, 143 | { 144 | "cell_type": "raw", 145 | "metadata": {}, 146 | "source": [ 147 | "# end::MONTE_CARLO[]" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 10, 153 | "metadata": {}, 154 | "outputs": [ 155 | { 156 | "name": "stdout", 157 | "output_type": "stream", 158 | "text": [ 159 | "Value of the European call option 7.989.\n" 160 | ] 161 | } 162 | ], 163 | "source": [ 164 | "%run bsm_mcs_euro.py" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "## Time-to-Results" 172 | ] 173 | }, 174 | { 175 | "cell_type": "raw", 176 | "metadata": {}, 177 | "source": [ 178 | "# tag::SPX_EXAMPLE[]" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 11, 184 | "metadata": { 185 | "uuid": "e16924db-8402-4bcb-a9c7-d5753346c72a" 186 | }, 187 | "outputs": [], 188 | "source": [ 189 | "import numpy as np # <1>\n", 190 | "import pandas as pd # <1>\n", 191 | "from pylab import plt, mpl # <2>" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 12, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "plt.style.use('seaborn') # <2>\n", 201 | "mpl.rcParams['font.family'] = 'serif' # <2>\n", 202 | "%matplotlib inline" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 13, 208 | "metadata": { 209 | "uuid": "22071d72-094b-4b51-aa39-cfa793d58623" 210 | }, 211 | "outputs": [ 212 | { 213 | "name": "stdout", 214 | "output_type": "stream", 215 | "text": [ 216 | "\n", 217 | "DatetimeIndex: 2138 entries, 2010-01-04 to 2018-06-29\n", 218 | "Data columns (total 1 columns):\n", 219 | ".SPX 2138 non-null float64\n", 220 | "dtypes: float64(1)\n", 221 | "memory usage: 33.4 KB\n" 222 | ] 223 | } 224 | ], 225 | "source": [ 226 | "data = pd.read_csv('http://hilpisch.com/tr_eikon_eod_data.csv',\n", 227 | " index_col=0, parse_dates=True) # <3>\n", 228 | "data = pd.DataFrame(data['.SPX']) # <4>\n", 229 | "data.dropna(inplace=True) # <4>\n", 230 | "data.info() # <5>" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 14, 236 | "metadata": { 237 | "uuid": "304b2f50-5b81-4e88-8fa7-ea69995cf2fe" 238 | }, 239 | "outputs": [], 240 | "source": [ 241 | "data['rets'] = np.log(data / data.shift(1)) # <6>\n", 242 | "data['vola'] = data['rets'].rolling(252).std() * np.sqrt(252) # <7>" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 15, 248 | "metadata": { 249 | "uuid": "b4e61939-c7fe-4a1e-a8c9-6fd755edcf86" 250 | }, 251 | "outputs": [ 252 | { 253 | "data": { 254 | "image/png": "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\n", 255 | "text/plain": [ 256 | "" 257 | ] 258 | }, 259 | "metadata": {}, 260 | "output_type": "display_data" 261 | } 262 | ], 263 | "source": [ 264 | "data[['.SPX', 'vola']].plot(subplots=True, figsize=(10, 6)); # <8>\n", 265 | "plt.savefig('../images/spx_volatility.png')" 266 | ] 267 | }, 268 | { 269 | "cell_type": "raw", 270 | "metadata": {}, 271 | "source": [ 272 | "# end::SPX_EXAMPLE[]" 273 | ] 274 | }, 275 | { 276 | "cell_type": "markdown", 277 | "metadata": {}, 278 | "source": [ 279 | "## Idioms & Paradigms" 280 | ] 281 | }, 282 | { 283 | "cell_type": "raw", 284 | "metadata": {}, 285 | "source": [ 286 | "# tag::PARADIGM_1[]" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 16, 292 | "metadata": { 293 | "uuid": "beca497d-4c20-4240-b003-4c79c26154be" 294 | }, 295 | "outputs": [ 296 | { 297 | "name": "stdout", 298 | "output_type": "stream", 299 | "text": [ 300 | "1.21 s ± 8.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "import math\n", 306 | "loops = 2500000\n", 307 | "a = range(1, loops)\n", 308 | "def f(x):\n", 309 | " return 3 * math.log(x) + math.cos(x) ** 2\n", 310 | "%timeit r = [f(x) for x in a]" 311 | ] 312 | }, 313 | { 314 | "cell_type": "raw", 315 | "metadata": {}, 316 | "source": [ 317 | "# end::PARADIGM_1[]" 318 | ] 319 | }, 320 | { 321 | "cell_type": "raw", 322 | "metadata": {}, 323 | "source": [ 324 | "# tag::PARADIGM_2[]" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 17, 330 | "metadata": { 331 | "uuid": "931fd1fc-cc54-4045-802f-acb8f094a23f" 332 | }, 333 | "outputs": [ 334 | { 335 | "name": "stdout", 336 | "output_type": "stream", 337 | "text": [ 338 | "94.7 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 339 | ] 340 | } 341 | ], 342 | "source": [ 343 | "import numpy as np\n", 344 | "a = np.arange(1, loops)\n", 345 | "%timeit r = 3 * np.log(a) + np.cos(a) ** 2" 346 | ] 347 | }, 348 | { 349 | "cell_type": "raw", 350 | "metadata": {}, 351 | "source": [ 352 | "# end::PARADIGM_2[]" 353 | ] 354 | }, 355 | { 356 | "cell_type": "raw", 357 | "metadata": {}, 358 | "source": [ 359 | "# tag::PARADIGM_3[]" 360 | ] 361 | }, 362 | { 363 | "cell_type": "code", 364 | "execution_count": 18, 365 | "metadata": { 366 | "uuid": "8d1602b3-a490-4d1a-97d9-112825d86185" 367 | }, 368 | "outputs": [ 369 | { 370 | "name": "stdout", 371 | "output_type": "stream", 372 | "text": [ 373 | "37.5 ms ± 936 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 374 | ] 375 | } 376 | ], 377 | "source": [ 378 | "import numexpr as ne\n", 379 | "ne.set_num_threads(1)\n", 380 | "f = '3 * log(a) + cos(a) ** 2'\n", 381 | "%timeit r = ne.evaluate(f)" 382 | ] 383 | }, 384 | { 385 | "cell_type": "raw", 386 | "metadata": {}, 387 | "source": [ 388 | "# end::PARADIGM_3[]" 389 | ] 390 | }, 391 | { 392 | "cell_type": "raw", 393 | "metadata": {}, 394 | "source": [ 395 | "# tag::PARADIGM_4[]" 396 | ] 397 | }, 398 | { 399 | "cell_type": "code", 400 | "execution_count": 19, 401 | "metadata": { 402 | "uuid": "6994f16a-1802-4abe-851d-a48e0ead154d" 403 | }, 404 | "outputs": [ 405 | { 406 | "name": "stdout", 407 | "output_type": "stream", 408 | "text": [ 409 | "12.8 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" 410 | ] 411 | } 412 | ], 413 | "source": [ 414 | "ne.set_num_threads(4)\n", 415 | "%timeit r = ne.evaluate(f)" 416 | ] 417 | }, 418 | { 419 | "cell_type": "raw", 420 | "metadata": {}, 421 | "source": [ 422 | "# end::PARADIGM_4[]" 423 | ] 424 | }, 425 | { 426 | "cell_type": "markdown", 427 | "metadata": {}, 428 | "source": [ 429 | "\"The
\n", 430 | "\n", 431 | "http://tpq.io | @dyjh | training@tpq.io" 432 | ] 433 | } 434 | ], 435 | "metadata": { 436 | "kernelspec": { 437 | "display_name": "Python 3", 438 | "language": "python", 439 | "name": "python3" 440 | }, 441 | "language_info": { 442 | "codemirror_mode": { 443 | "name": "ipython", 444 | "version": 3 445 | }, 446 | "file_extension": ".py", 447 | "mimetype": "text/x-python", 448 | "name": "python", 449 | "nbconvert_exporter": "python", 450 | "pygments_lexer": "ipython3", 451 | "version": "3.6.7" 452 | } 453 | }, 454 | "nbformat": 4, 455 | "nbformat_minor": 1 456 | } 457 | -------------------------------------------------------------------------------- /files/py4fi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Python for Finance 10 | 11 | 433 | 434 | 525 | 526 | 527 | 547 |
548 |
549 |

1. Why Python for Finance

550 |
551 |
552 |
553 |
554 |

Banks are essentially technology firms.

555 |
556 |
557 |
558 | — Hugo Banziger 559 |
560 |
561 |
562 |

1.1. Finance and Python Syntax

563 |
564 |

Most people who make their first steps with Python in a finance context may attack an algorithmic problem. This is similar to a scientist who, for example, wants to solve a differential equation, wants to evaluate an integral or simply wants to visualize some data. In general, at this stage, there is only little thought spent on topics like a formal development process, testing, documentation or deployment. However, this especially seems to be the stage when people fall in love with Python. A major reason for this might be that Python syntax is generally quite close to the mathematical syntax used to describe scientific problems or financial algorithms.

565 |
566 |
567 |

This aspect can be illustrated by a financial algorithm, namely the valuation of a European call option by Monte Carlo simulation. The example considers a Black-Scholes-Merton (BSM) setup in which the option’s underlying risk factor follows a geometric Brownian motion.

568 |
569 |
570 |

Assume the following numerical parameter values for the valuation:

571 |
572 |
573 |
    574 |
  • 575 |

    initial stock index level \(S_0 = 100\)

    576 |
  • 577 |
  • 578 |

    strike price of the European call option \(K = 105\)

    579 |
  • 580 |
  • 581 |

    time-to-maturity \(T = 1\) year

    582 |
  • 583 |
  • 584 |

    constant, riskless short rate \(r = 0.05\)

    585 |
  • 586 |
  • 587 |

    constant volatility \(\sigma = 0.2\)

    588 |
  • 589 |
590 |
591 |
592 |

In the BSM model, the index level at maturity is a random variable, given by Black-Scholes-Merton (1973) index level at maturity with \(z\) being a standard normally distributed random variable.

593 |
594 |
595 |
Black-Scholes-Merton (1973) index level at maturity
596 |
597 | \[\begin{equation} 598 | S_T = S_0 \exp \left( \left( r - \frac{1}{2} \sigma^2 \right) T + \sigma \sqrt{T} z \right) 599 | \end{equation}\] 600 |
601 |
602 |
603 |

The following is an algorithmic description of the Monte Carlo valuation procedure:

604 |
605 |
606 |
    607 |
  1. 608 |

    draw \(I\) pseudo-random numbers \(z(i), i \in \{1, 2, ..., I \}\), from the standard normal distribution

    609 |
  2. 610 |
  3. 611 |

    calculate all resulting index levels at maturity \(S_T(i)\) for given \(z(i)\) and Black-Scholes-Merton (1973) index level at maturity

    612 |
  4. 613 |
  5. 614 |

    calculate all inner values of the option at maturity as \(h_T(i)= \max \left(S_T(i) - K, 0 \right)\)

    615 |
  6. 616 |
  7. 617 |

    estimate the option present value via the Monte Carlo estimator as given in Monte Carlo estimator for European option

    618 |
  8. 619 |
620 |
621 |
622 |
Monte Carlo estimator for European option
623 |
624 | \[\begin{equation} 625 | C_0 \approx e^{-rT} \frac{1}{I} \sum_I h_T(i) 626 | \end{equation}\] 627 |
628 |
629 |
630 |

This problem and algorithm is now to be translated into Python code. The code below implements the required steps.

631 |
632 |
633 |
634 |
In [1]: import math
 635 |         import numpy as np  (1)
 636 | 
 637 | In [2]: S0 = 100.  (2)
 638 |         K = 105.  (2)
 639 |         T = 1.0  (2)
 640 |         r = 0.05  (2)
 641 |         sigma = 0.2  (2)
 642 | 
 643 | In [3]: I = 100000  (2)
 644 | 
 645 | In [4]: np.random.seed(1000)  (3)
 646 | 
 647 | In [5]: z = np.random.standard_normal(I)  (4)
 648 | 
 649 | In [6]: ST = S0 * np.exp((r - sigma ** 2 / 2) * T + sigma * math.sqrt(T) * z)  (5)
 650 | 
 651 | In [7]: hT = np.maximum(ST - K, 0)  (6)
 652 | 
 653 | In [8]: C0 = math.exp(-r * T) * np.mean(hT)  (7)
 654 | 
 655 | In [9]: print('Value of the European call option: {:5.3f}.'.format(C0))  (8)
 656 |         Value of the European call option: 8.019.
657 |
658 |
659 |
660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 |
1The model and simulation parameter values are defined.
2NumPy is used here as the main package.
3The seed value for the random number generator is fixed.
4Standard normally distributed random numbers are drawn.
5End-of-period values are simulated.
6The option payoffs at maturity are calculated.
7The Monte Carlo estimator is evaluated.
8The resulting value estimate is printed.
694 |
695 |
696 |

Three aspects are worth highlighting:

697 |
698 |
699 |
700 |
syntax
701 |
702 |

the Python syntax is indeed quite close to the mathematical syntax, e.g., when it comes to the parameter value assignments

703 |
704 |
translation
705 |
706 |

every mathematical and/or algorithmic statement can generally be translated into a single line of Python code

707 |
708 |
vectorization
709 |
710 |

one of the strengths of NumPy is the compact, vectorized syntax, e.g., allowing for 100,000 calculations within a single line of code

711 |
712 |
713 |
714 |
715 |

This code can be used in an interactive environment like IPython or Jupyter Notebook. However, code that is meant to be reused regularly typically gets organized in so-called modules (or scripts), which are single Python files (technically text files) with the suffix .py. Such a module could in this case look like Monte Carlo valuation of European call option and could be saved as a file named bsm_mcs_euro.py.

716 |
717 |
718 |
Example 1. Monte Carlo valuation of European call option
719 |
720 |
721 |
722 |
#
 723 | # Monte Carlo valuation of European call option
 724 | # in Black-Scholes-Merton model
 725 | # bsm_mcs_euro.py
 726 | #
 727 | # Python for Finance, 2nd ed.
 728 | # (c) Dr. Yves J. Hilpisch
 729 | #
 730 | import math
 731 | import numpy as np
 732 | 
 733 | # Parameter Values
 734 | S0 = 100.  # initial index level
 735 | K = 105.  # strike price
 736 | T = 1.0  # time-to-maturity
 737 | r = 0.05  # riskless short rate
 738 | sigma = 0.2  # volatility
 739 | 
 740 | I = 100000  # number of simulations
 741 | 
 742 | # Valuation Algorithm
 743 | z = np.random.standard_normal(I)  # pseudo-random numbers
 744 | # index values at maturity
 745 | ST = S0 * np.exp((r - 0.5 * sigma ** 2) * T + sigma * math.sqrt(T) * z)
 746 | hT = np.maximum(ST - K, 0)  # payoff at maturity
 747 | C0 = math.exp(-r * T) * np.mean(hT)  # Monte Carlo estimator
 748 | 
 749 | # Result Output
 750 | print('Value of the European call option %5.3f.' % C0)
751 |
752 |
753 |
754 |
755 |
756 |

The algorithmic example in this subsection illustrates that Python, with its very syntax, is well suited to complement the classic duo of scientific languages, English and mathematics. It seems that adding Python to the set of scientific languages makes it more well rounded. One then has:

757 |
758 |
759 |
    760 |
  • 761 |

    English for writing, talking about scientific and financial problems, etc.

    762 |
  • 763 |
  • 764 |

    Mathematics for concisely, exactly describing and modeling abstract aspects, algorithms, complex quantities, etc.

    765 |
  • 766 |
  • 767 |

    Python for technically modeling and implementing abstract aspects, algorithms, complex quantities, etc.

    768 |
  • 769 |
770 |
771 |
772 | 773 | 774 | 777 | 783 | 784 |
775 | 776 | 778 |
Mathematics and Python Syntax
779 |
780 |

There is hardly any programming language that comes as close to mathematical syntax as Python. Numerical algorithms are therefore in general straightforward to translate from the mathematical representation into the Pythonic implementation. This makes prototyping, development and code maintenance in finance quite efficient with Python.

781 |
782 |
785 |
786 |
787 |

In some areas, it is common practice to use pseudo-code and therewith to introduce a fourth language family member. The role of pseudo-code is to represent, for example, financial algorithms in a more technical fashion that is both still close to the mathematical representation and already quite close to the technical implementation. In addition to the algorithm itself, pseudo-code takes into account how computers work in principle.

788 |
789 |
790 |

This practice generally has its cause in the fact that with most (compiled) programming languages the technical implementation is quite “far away” from its formal, mathematical representation. The majority of programming languages make it necessary to include so many elements that are only technically required that it is hard to see the equivalence between the mathematics and the code.

791 |
792 |
793 |

Nowadays, Python is often used in a pseudo-code way since its syntax is almost analogous to the mathematics and since the technical “overhead” is kept to a minimum. This is accomplished by a number of high-level concepts embodied in the language that not only have their advantages but also come in general with risks and/or other costs. However, it is safe to say that with Python you can, whenever the need arises, follow the same strict implementation and coding practices that other languages might require from the outset. In that sense, Python can provide the best of both worlds: high-level abstraction and rigorous implementation.

794 |
795 |
796 |
797 |

1.2. Efficiency and Productivity Through Python

798 |
799 |

At a high level, benefits from using Python can be measured in three dimensions:

800 |
801 |
802 |
803 |
efficiency
804 |
805 |

how can Python help in getting results faster, in saving costs, and in saving time?

806 |
807 |
productivity
808 |
809 |

how can Python help in getting more done with the same resources (people, assets, etc.)?

810 |
811 |
quality
812 |
813 |

what does Python allow to do that alternative technologies do not allow for?

814 |
815 |
816 |
817 |
818 |

A discussion of these aspects can by nature not be exhaustive. However, it can highlight some arguments as a starting point.

819 |
820 |
821 |

1.2.1. Shorter Time-to-Results

822 |
823 |

A field where the efficiency of Python becomes quite obvious is interactive data analytics. This is a field that benefits tremendously from such powerful tools as IPython, Jupyter Notebook and packages like pandas.

824 |
825 |
826 |

Consider a finance student, writing her master’s thesis and interested in S&P 500 index values. She wants to analyze historical index levels for, say, a few years to see how the volatility of the index has fluctuated over time. She wants to find evidence that volatility, in contrast to some typical model assumptions, fluctuates over time and is far from being constant. The results should also be visualized. She mainly has to do the following:

827 |
828 |
829 |
    830 |
  • 831 |

    retrieve index level data from the web

    832 |
  • 833 |
  • 834 |

    calculate the annualized rolling standard deviation of the log returns (volatility)

    835 |
  • 836 |
  • 837 |

    plot the index level data and the volatility results

    838 |
  • 839 |
840 |
841 |
842 |

These tasks are complex enough that not too long ago one would have considered them to be something for professional financial analysts only. Today, even the finance student can easily cope with such problems. The code below shows how exactly this works — without worrying about syntax details at this stage (everything is explained in detail in subsequent chapters).

843 |
844 |
845 |
846 |
In [11]: import numpy as np  (1)
 847 |          import pandas as pd  (1)
 848 |          from pylab import plt, mpl  (2)
 849 | 
 850 | In [12]: plt.style.use('seaborn')  (2)
 851 |          mpl.rcParams['font.family'] = 'serif'  (2)
 852 |          %matplotlib inline
 853 | 
 854 | In [13]: data = pd.read_csv('http://hilpisch.com/tr_eikon_eod_data.csv',
 855 |                            index_col=0, parse_dates=True)  (3)
 856 |          data = pd.DataFrame(data['.SPX']) (4)
 857 |          data.dropna(inplace=True)  (4)
 858 |          data.info()  (5)
 859 |          <class 'pandas.core.frame.DataFrame'>
 860 |          DatetimeIndex: 2138 entries, 2010-01-04 to 2018-06-29
 861 |          Data columns (total 1 columns):
 862 |          .SPX    2138 non-null float64
 863 |          dtypes: float64(1)
 864 |          memory usage: 33.4 KB
 865 | 
 866 | In [14]: data['rets'] = np.log(data / data.shift(1))  (6)
 867 |          data['vola'] = data['rets'].rolling(252).std() * np.sqrt(252)  (7)
 868 | 
 869 | In [15]: data[['.SPX', 'vola']].plot(subplots=True, figsize=(10, 6));  (8)
 870 |          plt.savefig('../images/spx_volatility.png')
871 |
872 |
873 |
874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 |
1This imports NumPy and pandas.
2This imports matplotlib and configures the plotting style and approach for Jupyter.
3pd.read_csv() allows the retrieval of remotely or locally stored data sets in CSV form.
4A sub-set of the data is picked and NaN ("not a number") values eliminated.
5This shows some meta-information about the data set.
6The log returns are calculated in vectorized fashion (“no looping” on the Python level).
7The rolling, annualized volatility is derived.
8This finally plots the two time series.
908 |
909 |
910 |

S&P 500 closing values and annualized volatility shows the graphical result of this brief interactive session. It can be considered almost amazing that a few lines of code suffice to implement three rather complex tasks typically encountered in financial analytics: data gathering, complex and repeated mathematical calculations as well as visualization of the results. The example illustrates that pandas makes working with whole time series almost as simple as doing mathematical operations on floating-point numbers.

911 |
912 |
913 |
914 | spx volatility 915 |
916 |
Figure 1. S&P 500 closing values and annualized volatility
917 |
918 |
919 |

Translated to a professional finance context, the example implies that financial analysts can — when applying the right Python tools and packages that provide high-level abstractions — focus on their very domain and not on the technical intrinsicalities. Analysts can react faster, providing valuable insights almost in real-time and making sure they are one step ahead of the competition. This example of increased efficiency can easily translate into measurable bottom-line effects.

920 |
921 |
922 |
923 |

1.2.2. Ensuring High Performance

924 |
925 |

In general, it is accepted that Python has a rather concise syntax and that it is relatively efficient to code with. However, due to the very nature of Python being an interpreted language, the prejudice persists that Python often is too slow for compute-intensive tasks in finance. Indeed, depending on the specific implementation approach, Python can be really slow. But it does not have to be slow — it can be highly performing in almost any application area. In principle, one can distinguish at least three different strategies for better performance:

926 |
927 |
928 |
929 |
idioms and paradigms
930 |
931 |

in general, many different ways can lead to the same result in Python, but sometimes with rather different performance characteristics; “simply” choosing the right way (e.g., a specific implementation approach, such as the judicious use of data structures, avoiding loops through vectorization or the use of a specific package such as pandas) can improve results significantly

932 |
933 |
compiling
934 |
935 |

nowadays, there are several performance packages available that provide compiled versions of important functions or that compile Python code statically or dynamically (at runtime or call time) to machine code, which can make such functions orders of magnitude faster than pure Python code; popular ones are Cython and Numba

936 |
937 |
parallelization
938 |
939 |

many computational tasks, in particular in finance, can significantly benefit from parallel execution; this is nothing special to Python but something that can easily be accomplished with it

940 |
941 |
942 |
943 |
944 | 945 | 946 | 949 | 955 | 956 |
947 | 948 | 950 |
Performance Computing with Python
951 |
952 |

Python per se is not a high-performance computing technology. However, Python has developed into an ideal platform to access current performance technologies. In that sense, Python has become something like a glue language for performance computing technologies.

953 |
954 |
957 |
958 |
959 |

Later chapters illustrate all three strategies in detail. This sub-section sticks to a simple, but still realistic, example that touches upon all three strategies. A quite common task in financial analytics is to evaluate complex mathematical expressions on large arrays of numbers. To this end, Python itself provides everything needed.

960 |
961 |
962 |
963 |
In [16]: import math
 964 |          loops = 2500000
 965 |          a = range(1, loops)
 966 |          def f(x):
 967 |              return 3 * math.log(x) + math.cos(x) ** 2
 968 |          %timeit r = [f(x) for x in a]
 969 |          1.21 s ± 8.27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
970 |
971 |
972 |
973 |

The Python interpreter needs about 1.4 seconds in this case to evaluate the function f 2,500,000 times. The same task can be implemented using NumPy, which provides optimized (i.e., pre-compiled), functions to handle such array-based operations.

974 |
975 |
976 |
977 |
In [17]: import numpy as np
 978 |          a = np.arange(1, loops)
 979 |          %timeit r = 3 * np.log(a) + np.cos(a) ** 2
 980 |          94.7 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
981 |
982 |
983 |
984 |

Using NumPy considerably reduces the execution time to about 80 milliseconds. However, there is even a package specifically dedicated to this kind of task. It is called numexpr, for "`numerical expressions." It compiles the expression to improve upon the performance of the general NumPy functionality by, for example, avoiding in-memory copies of ndarray objects along the way.

985 |
986 |
987 |
988 |
In [18]: import numexpr as ne
 989 |          ne.set_num_threads(1)
 990 |          f = '3 * log(a) + cos(a) ** 2'
 991 |          %timeit r = ne.evaluate(f)
 992 |          37.5 ms ± 936 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
993 |
994 |
995 |
996 |

Using this more specialized approach further reduces execution time to about 40 milliseconds. However, numexpr also has built-in capabilities to parallelize the execution of the respective operation. This allows us to use multiple threads of a CPU.

997 |
998 |
999 |
1000 |
In [19]: ne.set_num_threads(4)
1001 |          %timeit r = ne.evaluate(f)
1002 |          12.8 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1003 |
1004 |
1005 |
1006 |

Parallelization brings execution time further down to below 15 milliseconds in this case, with four threads utilized. Overall, this is a performance improvement of more than 90 times. Note, in particular, that this kind of improvement is possible without altering the basic problem/algorithm and without knowing any detail about compiling or parallelization approaches. The capabilities are accessible from a high level even by non-experts. However, one has to be aware, of course, of which capabilities and options exist.

1007 |
1008 |
1009 |

The example shows that Python provides a number of options to make more out of existing resources — i.e., to increase productivity. With the parallel approach, three times as many calculations can be accomplished in the same amount of time as compared to the sequential approach — in this case simply by telling Python to use multiple available CPU threads instead of just one.

1010 |
1011 |
1012 |
1013 |
1014 |

1.3. Conclusions

1015 |
1016 |

Python as a language — and even more so as an ecosystem — is an ideal technological framework for the financial industry as whole and the individual working in finance alike. It is characterized by a number of benefits, like an elegant syntax, efficient development approaches and usability for prototyping as well as production. With its huge amount of available packages, libraries and tools, Python seems to have answers to most questions raised by recent developments in the financial industry in terms of analytics, data volumes and frequency, compliance and regulation, as well as technology itself. It has the potential to provide a single, powerful, consistent framework with which to streamline end-to-end development and production efforts even across larger financial institutions.

1017 |
1018 |
1019 |

In addition, Python has become the programming language of choice for artificial intelligence in general and machine and deep learning in particular. Python is therefore both the right language for data-driven finance as well as for AI-first finance, two recent trends that are about to reshape finance and the financial industry in fundamental ways.

1020 |
1021 |
1022 |
1023 |

1.4. Further Resources

1024 |
1025 |

The following books cover several aspects only touched upon in this chapter in more detail (e.g. Python tools, derivatives analytics, machine learning in general, machine learning in finance):

1026 |
1027 |
1028 |
    1029 |
  • 1030 |

    Hilpisch, Yves (2015): Derivatives Analytics with Python. Wiley Finance, Chichester, England. http://dawp.tpq.io.

    1031 |
  • 1032 |
  • 1033 |

    López de Prado, Marcos (2018): Advances in Financial Machine Learning. John Wiley & Sons, Hoboken.

    1034 |
  • 1035 |
  • 1036 |

    VanderPlas, Jake (2016): Python Data Science Handbook. O’Reilly, Beijing et al.

    1037 |
  • 1038 |
1039 |
1040 |
1041 |

When it comes to algorithmic trading, the author’s company offers a range of online training programs that focus on Python and other tools and techniques required in this rapidly growing field:

1042 |
1043 |
1044 | 1052 |
1053 |
1054 |

Sources referenced in this chapter are, among others, the following:

1055 |
1056 |
1057 |
    1058 |
  • 1059 |

    Ding, Cubillas (2010): “Optimizing the OTC Pricing and Valuation Infrastructure.” Celent study.

    1060 |
  • 1061 |
  • 1062 |

    Lewis, Michael (2014): Flash Boys. W. W. Norton & Company, New York.

    1063 |
  • 1064 |
  • 1065 |

    Patterson, Scott (2010): The Quants. Crown Business, New York.

    1066 |
  • 1067 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 | 1078 | 1093 | 1094 | 1095 | --------------------------------------------------------------------------------