Initial commit

This commit is contained in:
Thomas Rijpstra 2025-03-05 19:37:27 +01:00
commit 9767adb7de
Signed by: thomas
SSH Key Fingerprint: SHA256:sFF5HPNPaaW14qykTkmRi1FGGO0YMUPBenlKOqepUpw
13 changed files with 1794 additions and 0 deletions

39
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Test
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
on:
pull_request:
workflow_call:
jobs:
test:
name: run tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10" ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
coverage run -m pytest -v -s
- name: Generate Coverage Report
run: |
coverage report -m

174
.gitignore vendored Normal file
View File

@ -0,0 +1,174 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc

File diff suppressed because it is too large Load Diff

2
pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.ruff]
line-length = 100

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
pythonpath = .
testpaths = src
python_files = *_test.py

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
numpy==2.2.3
pandas==2.2.3
pytest==8.3.5
scipy==1.15.2

0
src/__init__.py Normal file
View File

30
src/assignment.md Normal file
View File

@ -0,0 +1,30 @@
#%% md
## Answer 1 - Option
* Implied Volatility (IV) provides a prediction of the future, while Historical Volatility (HV) provides an observation of the past.
* IV is derived from option pricing models while HV is calculated directly from historical price data.
* IV provides a subjective measure on future volatility, while HV provides an objective measure of baseline volatility.
## Answer 2 - VaR
Value-at-Risk (VaR) is a measurement of the maximum potential loss in value of a portfolio over a defined period (e.g. 1 day) and for a given confidence interval (e.g. 95%).
There are various methods of calculation that mainly differ in the way they approximate portfolio volatility.
The simplest method (parametric) assumes that portfolio volatility is constant and thus return variability can be approximated using a normal distribution. It requires very little data (only the current portfolio value and a measure of volatility, e.g. annual volatility of the portfolio) and is easily computed but has practical limitations due to its assumptions.
A more complex method is the Peaks Over Threshold (POT) method. This method requires more data, specifically containing sufficient extreme events, as it uses historical excesses in portfolio returns to extrapolate risk beyond historical observations.
In other words, it estimates the chance of future excesses in returns.
This estimation is dependent on the method's parameters, so stress testing is needed make sure the parameters are aptly chosen.
Because VaR is used in risk management, and specifically to reduce portfolio risk if it's deemed to large, a method that specifically models extreme events (i.e. "tail focus") is preferable.
## Answer 3 - Option
Refer to `options.py` for the implementation and `options_test.py` for both unit- and end-to-end tests.
## Answer 4 - VaR
Refer to `var.py` for the implementation and `var_test.py` for both unit- and end-to-end tests.
#%%

261
src/data/var_fx_prices.csv Normal file
View File

@ -0,0 +1,261 @@
date,ccy-1,ccy-2
14-11-2019,1.1684427,0.886564121
13-11-2019,1.165976797,0.884329678
12-11-2019,1.16603118,0.883470271
11-11-2019,1.166901992,0.87753938
8-11-2019,1.160725686,0.877658927
7-11-2019,1.160712213,0.876270592
6-11-2019,1.162466288,0.877654906
5-11-2019,1.162020521,0.876693114
4-11-2019,1.158050769,0.881600987
1-11-2019,1.159084323,0.881911985
31-10-2019,1.160995205,0.885700368
30-10-2019,1.156992283,0.886839305
29-10-2019,1.160671797,0.878271562
28-10-2019,1.159164938,0.879816998
25-10-2019,1.157420803,0.881290209
24-10-2019,1.157126161,0.880902044
23-10-2019,1.158761979,0.88163985
22-10-2019,1.15928588,0.878464444
21-10-2019,1.164903779,0.87788605
18-10-2019,1.157889862,0.879198171
17-10-2019,1.155081202,0.883197174
16-10-2019,1.158426394,0.879700902
15-10-2019,1.157836236,0.881329044
14-10-2019,1.141630705,0.885151582
11-10-2019,1.148791471,0.88210647
10-10-2019,1.115299681,0.879971841
9-10-2019,1.112743134,0.875465091
8-10-2019,1.114876918,0.876001927
7-10-2019,1.12083749,0.876270592
4-10-2019,1.119870991,0.876193814
3-10-2019,1.129318231,0.876616261
2-10-2019,1.124062813,0.876923751
1-10-2019,1.119733056,0.874431619
30-9-2019,1.128935752,0.873667657
27-9-2019,1.124492573,0.880824452
26-9-2019,1.12731946,0.877654906
25-9-2019,1.129496809,0.876424189
24-9-2019,1.130867236,0.872600349
23-9-2019,1.133758437,0.873896705
20-9-2019,1.135151089,0.868809731
19-9-2019,1.129484052,0.867829558
18-9-2019,1.128579006,0.871269876
17-9-2019,1.129905201,0.872105699
16-9-2019,1.129292724,0.874087671
13-9-2019,1.124151266,0.87753938
12-9-2019,1.123458883,0.878734622
11-9-2019,1.122258883,0.87966221
10-9-2019,1.118918677,0.879894413
9-9-2019,1.116220922,0.88051422
6-9-2019,1.11477749,0.878811846
5-9-2019,1.116245842,0.880630531
4-9-2019,1.105534305,0.877308418
3-9-2019,1.103046615,0.874469853
2-9-2019,1.099601944,0.87507623
30-8-2019,1.103022281,0.875273523
29-8-2019,1.10282765,0.87001914
28-8-2019,1.103387399,0.871991629
27-8-2019,1.010123245,0.874928912
26-8-2019,1.099747058,0.876501008
23-8-2019,1.104899123,0.878889084
22-8-2019,1.105546527,0.880630531
21-8-2019,1.093469798,0.882768362
20-8-2019,1.094235567,0.886839305
19-8-2019,1.093637219,0.884955752
16-8-2019,1.093744873,0.886014265
15-8-2019,1.090655266,0.886839305
14-8-2019,1.082508822,0.887941751
13-8-2019,1.078865034,0.884564352
12-8-2019,1.077156737,0.882339966
9-8-2019,1.07820199,0.881057269
8-8-2019,1.083247576,0.881290209
7-8-2019,1.083505791,0.881911985
6-8-2019,1.087961704,0.881367883
5-8-2019,1.085422772,0.88028169
2-8-2019,1.091381361,0.878618811
1-8-2019,1.098490674,0.876232202
31-7-2019,1.096234287,0.878194432
30-7-2019,1.090905124,0.883119177
29-7-2019,1.098719991,0.884603476
26-7-2019,1.113337787,0.883587365
25-7-2019,1.119670369,0.890194507
24-7-2019,1.121642084,0.890234132
23-7-2019,1.11564808,0.890947969
22-7-2019,1.113647753,0.88691796
19-7-2019,1.114243373,0.884173298
18-7-2019,1.112978442,0.884642604
17-7-2019,1.107162232,0.883002208
16-7-2019,1.106304831,0.882028666
15-7-2019,1.111296327,0.881406725
12-7-2019,1.116196004,0.880902044
11-7-2019,1.114293037,0.88028169
10-7-2019,1.1105805,0.886367665
9-7-2019,1.112433671,0.883197174
8-7-2019,1.115672974,0.886485528
5-7-2019,1.114504157,0.888967908
4-7-2019,1.114789918,0.88999644
3-7-2019,1.113635351,0.890323
2-7-2019,1.115038525,0.891067053
1-7-2019,1.117006423,0.893535272
28-6-2019,1.114050958,0.890511599
27-6-2019,1.115050958,0.891583452
26-6-2019,1.114541422,0.89098766
25-6-2019,1.117355889,0.887981175
24-6-2019,1.117218573,0.88711466
21-6-2019,1.121453404,0.888809884
20-6-2019,1.124985938,0.88711466
19-6-2019,1.126164172,0.883821645
18-6-2019,1.120925436,0.884407889
17-6-2019,1.118480656,0.884955752
14-6-2019,1.123204277,0.884760009
13-6-2019,1.125365744,0.889363216
12-6-2019,1.124163903,0.889086464
11-6-2019,1.124075448,0.888533475
10-6-2019,1.121239643,0.892299456
7-6-2019,1.125163149,0.892538379
6-6-2019,1.127116161,0.8942312
5-6-2019,1.130467222,0.89512332
4-6-2019,1.12847712,0.89561596
3-6-2019,1.127471982,0.8952134
31-5-2019,1.1281843,0.889600569
30-5-2019,1.131528922,0.894134478
29-5-2019,1.133542661,0.893894699
28-5-2019,1.134404211,0.893255918
27-5-2019,1.132964742,0.893854749
24-5-2019,1.132618274,0.892538379
23-5-2019,1.135976372,0.89047195
22-5-2019,1.134803282,0.889263216
21-5-2019,1.14310536,0.889363216
20-5-2019,1.140706097,0.891901534
17-5-2019,1.14087528,0.892259648
16-5-2019,1.144348065,0.89465444
15-5-2019,1.147802532,0.896137647
14-5-2019,1.152113552,0.89561596
13-5-2019,1.154921119,0.894374385
10-5-2019,1.158842548,0.896619744
9-5-2019,1.159393405,0.896901206
8-5-2019,1.160294715,0.892458724
7-5-2019,1.16638479,0.893575194
6-5-2019,1.169590643,0.894854586
3-5-2019,1.172525385,0.897504936
2-5-2019,1.165324601,0.898714838
1-5-2019,1.163995297,0.897021887
30-4-2019,1.16132379,0.892618049
29-4-2019,1.157407407,0.89031339
26-4-2019,1.57284634,0.887862914
25-4-2019,1.568324628,0.886406949
24-4-2019,1.156590833,0.882067566
23-4-2019,1.154227937,0.883704489
22-4-2019,1.152963693,0.883743538
19-4-2019,1.156122826,0.883431247
18-4-2019,1.156925355,0.887232721
17-4-2019,1.154521105,0.890868597
16-4-2019,1.155214639,0.890789239
15-4-2019,1.159729551,0.894054537
12-4-2019,1.158158065,0.891583452
11-4-2019,1.160739159,0.885778821
10-4-2019,1.163277651,0.883314195
9-4-2019,1.157420803,0.878040214
8-4-2019,1.158721698,0.878425861
5-4-2019,1.159218687,0.879043601
4-4-2019,1.166412,0.879584836
3-4-2019,1.171687347,0.878889084
2-4-2019,1.164225674,0.883041194
1-4-2019,1.170686022,0.885269122
29-3-2019,1.168346532,0.885367665
28-3-2019,1.164646006,0.886367665
27-3-2019,1.172374175,0.891106755
26-3-2019,1.171440286,0.891543708
25-3-2019,1.165555504,0.892379083
22-3-2019,1.171550078,0.888888889
21-3-2019,1.150483203,0.888730892
20-3-2019,1.161737029,0.888691402
19-3-2019,1.169057389,0.887862914
18-3-2019,1.165949608,0.891106755
15-3-2019,1.17249789,0.890828916
14-3-2019,1.174853437,0.891662951
13-3-2019,1.169590643,0.891106755
12-3-2019,1.161642563,0.891027355
11-3-2019,1.166520852,0.896860987
8-3-2019,1.158506916,0.897424392
7-3-2019,1.16780138,0.896097495
6-3-2019,1.162007019,0.898795614
5-3-2019,1.159810255,0.898311175
4-3-2019,1.163372384,0.897786955
1-3-2019,1.16295297,0.90244334
28-2-2019,1.16512094,0.90440445
27-2-2019,1.171508903,0.900738606
26-2-2019,1.165311022,0.894054537
25-2-2019,1.151768541,0.893974611
22-2-2019,1.151330939,0.891662951
21-2-2019,1.151410478,0.89273758
20-2-2019,1.151012891,0.892777431
19-2-2019,1.149729239,0.89146423
18-2-2019,1.143301397,0.893694982
15-2-2019,1.140693085,0.896860987
14-2-2019,1.133118796,0.900252071
13-2-2019,1.14153948,0.900576369
12-2-2019,1.140133852,0.901225667
11-2-2019,1.140914329,0.901794571
8-2-2019,1.142739604,0.901388138
7-2-2019,1.142400183,0.902445628
6-2-2019,1.139211666,0.900009
5-2-2019,1.134198349,0.899806542
4-2-2019,1.144885225,0.901243
1-2-2019,1.14187839,0.902934537
31-1-2019,1.143379831,0.903628067
30-1-2019,1.144112398,0.906043309
29-1-2019,1.148349572,0.911701691
28-1-2019,1.15036409,0.912325518
25-1-2019,1.155374804,0.906618314
24-1-2019,1.148567162,0.905387053
23-1-2019,1.14825064,0.905223138
22-1-2019,1.140797189,0.904486252
21-1-2019,1.134751773,0.905715062
18-1-2019,1.135383078,0.909338911
17-1-2019,1.134134032,0.906700517
16-1-2019,1.128515325,0.902282775
15-1-2019,1.119482351,0.909256228
14-1-2019,1.125707789,0.904977376
11-1-2019,1.117143687,0.90424089
10-1-2019,1.108696616,0.904568069
9-1-2019,1.107125459,0.908265213
8-1-2019,1.112644087,0.90962735
7-1-2019,1.1141813,0.91124248
4-1-2019,1.115026092,0.912575287
3-1-2019,1.106060103,0.913993236
2-1-2019,1.110592834,0.913408842
31-12-2018,1.117156167,0.916674306
28-12-2018,1.109299256,0.916086479
27-12-2018,1.107910481,0.913575735
26-12-2018,1.110975325,0.910332271
24-12-2018,1.114305453,0.910829766
21-12-2018,1.1105805,0.909545682
20-12-2018,1.106831363,0.912825194
19-12-2018,1.10702741,0.910705341
18-12-2018,1.111716379,0.908182726
17-12-2018,1.112334679,0.905879156
14-12-2018,1.111790539,0.907029478
13-12-2018,1.112099644,0.906084356
12-12-2018,1.113833816,0.904813608
11-12-2018,1.106427236,0.899523253
10-12-2018,1.101770545,0.897585495
7-12-2018,1.12027245,0.897182846
6-12-2018,1.123048133,0.898391879
5-12-2018,1.123078133,0.899523253
4-12-2018,1.1212145,0.900414191
3-12-2018,1.122498232,0.901631954
30-11-2018,1.122384843,0.901550667
29-11-2018,1.122120359,0.899604174
28-11-2018,1.130671732,0.899118864
27-11-2018,1.128387985,0.896539358
26-11-2018,1.130965845,0.895255148
23-11-2018,1.128795575,0.896458987
22-11-2018,1.128349788,0.903097625
21-11-2018,1.121730157,0.903056847
20-11-2018,1.123898579,0.912825194
19-11-2018,1.247645328,0.910705341
16-11-2018,1.126722477,0.900414191
15-11-2018,1.129598879,0.905879156
14-11-2018,1.14998045,0.895255148
1 date ccy-1 ccy-2
2 14-11-2019 1.1684427 0.886564121
3 13-11-2019 1.165976797 0.884329678
4 12-11-2019 1.16603118 0.883470271
5 11-11-2019 1.166901992 0.87753938
6 8-11-2019 1.160725686 0.877658927
7 7-11-2019 1.160712213 0.876270592
8 6-11-2019 1.162466288 0.877654906
9 5-11-2019 1.162020521 0.876693114
10 4-11-2019 1.158050769 0.881600987
11 1-11-2019 1.159084323 0.881911985
12 31-10-2019 1.160995205 0.885700368
13 30-10-2019 1.156992283 0.886839305
14 29-10-2019 1.160671797 0.878271562
15 28-10-2019 1.159164938 0.879816998
16 25-10-2019 1.157420803 0.881290209
17 24-10-2019 1.157126161 0.880902044
18 23-10-2019 1.158761979 0.88163985
19 22-10-2019 1.15928588 0.878464444
20 21-10-2019 1.164903779 0.87788605
21 18-10-2019 1.157889862 0.879198171
22 17-10-2019 1.155081202 0.883197174
23 16-10-2019 1.158426394 0.879700902
24 15-10-2019 1.157836236 0.881329044
25 14-10-2019 1.141630705 0.885151582
26 11-10-2019 1.148791471 0.88210647
27 10-10-2019 1.115299681 0.879971841
28 9-10-2019 1.112743134 0.875465091
29 8-10-2019 1.114876918 0.876001927
30 7-10-2019 1.12083749 0.876270592
31 4-10-2019 1.119870991 0.876193814
32 3-10-2019 1.129318231 0.876616261
33 2-10-2019 1.124062813 0.876923751
34 1-10-2019 1.119733056 0.874431619
35 30-9-2019 1.128935752 0.873667657
36 27-9-2019 1.124492573 0.880824452
37 26-9-2019 1.12731946 0.877654906
38 25-9-2019 1.129496809 0.876424189
39 24-9-2019 1.130867236 0.872600349
40 23-9-2019 1.133758437 0.873896705
41 20-9-2019 1.135151089 0.868809731
42 19-9-2019 1.129484052 0.867829558
43 18-9-2019 1.128579006 0.871269876
44 17-9-2019 1.129905201 0.872105699
45 16-9-2019 1.129292724 0.874087671
46 13-9-2019 1.124151266 0.87753938
47 12-9-2019 1.123458883 0.878734622
48 11-9-2019 1.122258883 0.87966221
49 10-9-2019 1.118918677 0.879894413
50 9-9-2019 1.116220922 0.88051422
51 6-9-2019 1.11477749 0.878811846
52 5-9-2019 1.116245842 0.880630531
53 4-9-2019 1.105534305 0.877308418
54 3-9-2019 1.103046615 0.874469853
55 2-9-2019 1.099601944 0.87507623
56 30-8-2019 1.103022281 0.875273523
57 29-8-2019 1.10282765 0.87001914
58 28-8-2019 1.103387399 0.871991629
59 27-8-2019 1.010123245 0.874928912
60 26-8-2019 1.099747058 0.876501008
61 23-8-2019 1.104899123 0.878889084
62 22-8-2019 1.105546527 0.880630531
63 21-8-2019 1.093469798 0.882768362
64 20-8-2019 1.094235567 0.886839305
65 19-8-2019 1.093637219 0.884955752
66 16-8-2019 1.093744873 0.886014265
67 15-8-2019 1.090655266 0.886839305
68 14-8-2019 1.082508822 0.887941751
69 13-8-2019 1.078865034 0.884564352
70 12-8-2019 1.077156737 0.882339966
71 9-8-2019 1.07820199 0.881057269
72 8-8-2019 1.083247576 0.881290209
73 7-8-2019 1.083505791 0.881911985
74 6-8-2019 1.087961704 0.881367883
75 5-8-2019 1.085422772 0.88028169
76 2-8-2019 1.091381361 0.878618811
77 1-8-2019 1.098490674 0.876232202
78 31-7-2019 1.096234287 0.878194432
79 30-7-2019 1.090905124 0.883119177
80 29-7-2019 1.098719991 0.884603476
81 26-7-2019 1.113337787 0.883587365
82 25-7-2019 1.119670369 0.890194507
83 24-7-2019 1.121642084 0.890234132
84 23-7-2019 1.11564808 0.890947969
85 22-7-2019 1.113647753 0.88691796
86 19-7-2019 1.114243373 0.884173298
87 18-7-2019 1.112978442 0.884642604
88 17-7-2019 1.107162232 0.883002208
89 16-7-2019 1.106304831 0.882028666
90 15-7-2019 1.111296327 0.881406725
91 12-7-2019 1.116196004 0.880902044
92 11-7-2019 1.114293037 0.88028169
93 10-7-2019 1.1105805 0.886367665
94 9-7-2019 1.112433671 0.883197174
95 8-7-2019 1.115672974 0.886485528
96 5-7-2019 1.114504157 0.888967908
97 4-7-2019 1.114789918 0.88999644
98 3-7-2019 1.113635351 0.890323
99 2-7-2019 1.115038525 0.891067053
100 1-7-2019 1.117006423 0.893535272
101 28-6-2019 1.114050958 0.890511599
102 27-6-2019 1.115050958 0.891583452
103 26-6-2019 1.114541422 0.89098766
104 25-6-2019 1.117355889 0.887981175
105 24-6-2019 1.117218573 0.88711466
106 21-6-2019 1.121453404 0.888809884
107 20-6-2019 1.124985938 0.88711466
108 19-6-2019 1.126164172 0.883821645
109 18-6-2019 1.120925436 0.884407889
110 17-6-2019 1.118480656 0.884955752
111 14-6-2019 1.123204277 0.884760009
112 13-6-2019 1.125365744 0.889363216
113 12-6-2019 1.124163903 0.889086464
114 11-6-2019 1.124075448 0.888533475
115 10-6-2019 1.121239643 0.892299456
116 7-6-2019 1.125163149 0.892538379
117 6-6-2019 1.127116161 0.8942312
118 5-6-2019 1.130467222 0.89512332
119 4-6-2019 1.12847712 0.89561596
120 3-6-2019 1.127471982 0.8952134
121 31-5-2019 1.1281843 0.889600569
122 30-5-2019 1.131528922 0.894134478
123 29-5-2019 1.133542661 0.893894699
124 28-5-2019 1.134404211 0.893255918
125 27-5-2019 1.132964742 0.893854749
126 24-5-2019 1.132618274 0.892538379
127 23-5-2019 1.135976372 0.89047195
128 22-5-2019 1.134803282 0.889263216
129 21-5-2019 1.14310536 0.889363216
130 20-5-2019 1.140706097 0.891901534
131 17-5-2019 1.14087528 0.892259648
132 16-5-2019 1.144348065 0.89465444
133 15-5-2019 1.147802532 0.896137647
134 14-5-2019 1.152113552 0.89561596
135 13-5-2019 1.154921119 0.894374385
136 10-5-2019 1.158842548 0.896619744
137 9-5-2019 1.159393405 0.896901206
138 8-5-2019 1.160294715 0.892458724
139 7-5-2019 1.16638479 0.893575194
140 6-5-2019 1.169590643 0.894854586
141 3-5-2019 1.172525385 0.897504936
142 2-5-2019 1.165324601 0.898714838
143 1-5-2019 1.163995297 0.897021887
144 30-4-2019 1.16132379 0.892618049
145 29-4-2019 1.157407407 0.89031339
146 26-4-2019 1.57284634 0.887862914
147 25-4-2019 1.568324628 0.886406949
148 24-4-2019 1.156590833 0.882067566
149 23-4-2019 1.154227937 0.883704489
150 22-4-2019 1.152963693 0.883743538
151 19-4-2019 1.156122826 0.883431247
152 18-4-2019 1.156925355 0.887232721
153 17-4-2019 1.154521105 0.890868597
154 16-4-2019 1.155214639 0.890789239
155 15-4-2019 1.159729551 0.894054537
156 12-4-2019 1.158158065 0.891583452
157 11-4-2019 1.160739159 0.885778821
158 10-4-2019 1.163277651 0.883314195
159 9-4-2019 1.157420803 0.878040214
160 8-4-2019 1.158721698 0.878425861
161 5-4-2019 1.159218687 0.879043601
162 4-4-2019 1.166412 0.879584836
163 3-4-2019 1.171687347 0.878889084
164 2-4-2019 1.164225674 0.883041194
165 1-4-2019 1.170686022 0.885269122
166 29-3-2019 1.168346532 0.885367665
167 28-3-2019 1.164646006 0.886367665
168 27-3-2019 1.172374175 0.891106755
169 26-3-2019 1.171440286 0.891543708
170 25-3-2019 1.165555504 0.892379083
171 22-3-2019 1.171550078 0.888888889
172 21-3-2019 1.150483203 0.888730892
173 20-3-2019 1.161737029 0.888691402
174 19-3-2019 1.169057389 0.887862914
175 18-3-2019 1.165949608 0.891106755
176 15-3-2019 1.17249789 0.890828916
177 14-3-2019 1.174853437 0.891662951
178 13-3-2019 1.169590643 0.891106755
179 12-3-2019 1.161642563 0.891027355
180 11-3-2019 1.166520852 0.896860987
181 8-3-2019 1.158506916 0.897424392
182 7-3-2019 1.16780138 0.896097495
183 6-3-2019 1.162007019 0.898795614
184 5-3-2019 1.159810255 0.898311175
185 4-3-2019 1.163372384 0.897786955
186 1-3-2019 1.16295297 0.90244334
187 28-2-2019 1.16512094 0.90440445
188 27-2-2019 1.171508903 0.900738606
189 26-2-2019 1.165311022 0.894054537
190 25-2-2019 1.151768541 0.893974611
191 22-2-2019 1.151330939 0.891662951
192 21-2-2019 1.151410478 0.89273758
193 20-2-2019 1.151012891 0.892777431
194 19-2-2019 1.149729239 0.89146423
195 18-2-2019 1.143301397 0.893694982
196 15-2-2019 1.140693085 0.896860987
197 14-2-2019 1.133118796 0.900252071
198 13-2-2019 1.14153948 0.900576369
199 12-2-2019 1.140133852 0.901225667
200 11-2-2019 1.140914329 0.901794571
201 8-2-2019 1.142739604 0.901388138
202 7-2-2019 1.142400183 0.902445628
203 6-2-2019 1.139211666 0.900009
204 5-2-2019 1.134198349 0.899806542
205 4-2-2019 1.144885225 0.901243
206 1-2-2019 1.14187839 0.902934537
207 31-1-2019 1.143379831 0.903628067
208 30-1-2019 1.144112398 0.906043309
209 29-1-2019 1.148349572 0.911701691
210 28-1-2019 1.15036409 0.912325518
211 25-1-2019 1.155374804 0.906618314
212 24-1-2019 1.148567162 0.905387053
213 23-1-2019 1.14825064 0.905223138
214 22-1-2019 1.140797189 0.904486252
215 21-1-2019 1.134751773 0.905715062
216 18-1-2019 1.135383078 0.909338911
217 17-1-2019 1.134134032 0.906700517
218 16-1-2019 1.128515325 0.902282775
219 15-1-2019 1.119482351 0.909256228
220 14-1-2019 1.125707789 0.904977376
221 11-1-2019 1.117143687 0.90424089
222 10-1-2019 1.108696616 0.904568069
223 9-1-2019 1.107125459 0.908265213
224 8-1-2019 1.112644087 0.90962735
225 7-1-2019 1.1141813 0.91124248
226 4-1-2019 1.115026092 0.912575287
227 3-1-2019 1.106060103 0.913993236
228 2-1-2019 1.110592834 0.913408842
229 31-12-2018 1.117156167 0.916674306
230 28-12-2018 1.109299256 0.916086479
231 27-12-2018 1.107910481 0.913575735
232 26-12-2018 1.110975325 0.910332271
233 24-12-2018 1.114305453 0.910829766
234 21-12-2018 1.1105805 0.909545682
235 20-12-2018 1.106831363 0.912825194
236 19-12-2018 1.10702741 0.910705341
237 18-12-2018 1.111716379 0.908182726
238 17-12-2018 1.112334679 0.905879156
239 14-12-2018 1.111790539 0.907029478
240 13-12-2018 1.112099644 0.906084356
241 12-12-2018 1.113833816 0.904813608
242 11-12-2018 1.106427236 0.899523253
243 10-12-2018 1.101770545 0.897585495
244 7-12-2018 1.12027245 0.897182846
245 6-12-2018 1.123048133 0.898391879
246 5-12-2018 1.123078133 0.899523253
247 4-12-2018 1.1212145 0.900414191
248 3-12-2018 1.122498232 0.901631954
249 30-11-2018 1.122384843 0.901550667
250 29-11-2018 1.122120359 0.899604174
251 28-11-2018 1.130671732 0.899118864
252 27-11-2018 1.128387985 0.896539358
253 26-11-2018 1.130965845 0.895255148
254 23-11-2018 1.128795575 0.896458987
255 22-11-2018 1.128349788 0.903097625
256 21-11-2018 1.121730157 0.903056847
257 20-11-2018 1.123898579 0.912825194
258 19-11-2018 1.247645328 0.910705341
259 16-11-2018 1.126722477 0.900414191
260 15-11-2018 1.129598879 0.905879156
261 14-11-2018 1.14998045 0.895255148

60
src/options.py Normal file
View File

@ -0,0 +1,60 @@
import pandas as pd
import numpy as np
from scipy.stats import norm
def as_df(option):
"""Creates a dataframe holding the option, and calculates time to maturity"""
df = pd.DataFrame([option])
df["T"] = (df["t1"] - df["t0"]) / np.timedelta64(365, "D")
return df
def black_scholes_option_price_spot(S, K, T, r, sigma, option_type="call"):
"""Calculates option price using Black-Scholes formula with spot price assuming zero dividend"""
sigma_scaled = sigma * np.sqrt(T)
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / sigma_scaled
d2 = d1 - sigma_scaled
if option_type == "call":
return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1) # put
def black_scholes_option_price_forward(F, K, T, r, sigma, option_type="call"):
"""Calculates option price using Black-Scholes formula with forward price assuming zero dividend"""
sigma_scaled = sigma * np.sqrt(T)
d1 = (np.log(F / K) + (0.5 * sigma**2) * T) / sigma_scaled
d2 = d1 - sigma_scaled
discount = np.exp(-r * T)
if option_type == "call":
return discount * (F * norm.cdf(d1) - K * norm.cdf(d2))
return discount * (K * norm.cdf(-d2) - F * norm.cdf(-d1)) # put
def forward_price(S, T, r):
"""Calculates the forward price assuming zero dividend"""
return S * np.exp(r * T)
def moneyness(strike_price, current_price, option_type="call", threshold=0.005):
"""Calculates if an option is ITM, ATM, or OTM"""
fuzzy_price = threshold * strike_price
if option_type == "put":
if current_price < strike_price - fuzzy_price:
return "ITM"
if current_price > strike_price + fuzzy_price:
return "OTM"
return "ATM"
if option_type == "call":
if current_price > strike_price + fuzzy_price:
return "ITM"
if current_price < strike_price - fuzzy_price:
return "OTM"
return "ATM"
return ""

114
src/options_test.py Normal file
View File

@ -0,0 +1,114 @@
import pytest
import pandas as pd
from pandas.testing import assert_series_equal
from . import options
# https://analystprep.com/study-notes/actuarial-exams/soa/ifm-investment-and-financial-markets/black-scholes-option-pricing-model/
@pytest.mark.parametrize(
"S, K, T, r, sigma, option_type, expected", [(100, 90, 0.5, 0.10, 0.25, "call", 16.11)]
)
def test_black_scholes_option_price_spot(S, K, T, r, sigma, option_type, expected):
assert (
abs(options.black_scholes_option_price_spot(S, K, T, r, sigma, option_type)) - expected
< 0.05
)
@pytest.mark.parametrize(
"F, K, T, r, sigma, option_type, expected", [(100, 90, 0.5, 0.10, 0.25, "call", 16.11)]
)
def test_black_scholes_option_price_forward(F, K, T, r, sigma, option_type, expected):
assert (
abs(options.black_scholes_option_price_forward(F, K, T, r, sigma, option_type)) - expected
< 0.05
)
@pytest.mark.parametrize(
"current_price, strike_price, option_type, expected",
[
(1, 10, "call", "ITM"),
(10, 1, "call", "OTM"),
(10, 10, "call", "ATM"),
(1, 10, "put", "OTM"),
(10, 1, "put", "ITM"),
(10, 10, "put", "ATM"),
],
)
def test_moneyness(current_price, strike_price, option_type, expected):
assert options.moneyness(current_price, strike_price, option_type) == expected
def test_end_to_end():
option = {
"t0": pd.Timestamp("2024-11-23"),
"t1": pd.Timestamp("2025-05-10"),
"S": 19,
"K": 17,
"r": 0.005,
"sigma": 0.3,
}
options_df = options.as_df(option)
expected = {
"F": 19.04367,
"C_F": 2.70,
"C_S": 2.70,
"P_F": 0.66,
"P_PCP": 0.66,
}
expected_df = pd.DataFrame([expected])
assert_series_equal(
options.forward_price(options_df["S"], options_df["T"], options_df["r"]),
expected_df["F"],
check_names=False,
check_exact=False,
)
assert_series_equal(
options.black_scholes_option_price_spot(
options_df["S"],
options_df["K"],
options_df["T"],
options_df["r"],
options_df["sigma"],
"call",
),
expected_df["C_S"],
rtol=0.01,
check_names=False,
check_exact=False,
)
assert_series_equal(
options.black_scholes_option_price_forward(
options.forward_price(options_df["S"], options_df["T"], options_df["r"]),
options_df["K"],
options_df["T"],
options_df["r"],
options_df["sigma"],
"call",
),
expected_df["C_F"],
rtol=0.01,
check_names=False,
check_exact=False,
)
assert_series_equal(
options.black_scholes_option_price_forward(
options.forward_price(options_df["S"], options_df["T"], options_df["r"]),
options_df["K"],
options_df["T"],
options_df["r"],
options_df["sigma"],
"put",
),
expected_df["P_F"],
rtol=0.01,
check_names=False,
check_exact=False,
)

20
src/var.py Normal file
View File

@ -0,0 +1,20 @@
import numpy as np
def calculate_returns(prices, horizon=1, sort="ascending"):
"""Calculate (simple) returns from price action"""
step = 1 if sort == "ascending" else -1
if horizon == 1:
returns = prices / prices.shift(step) - 1
else:
returns = np.exp(np.log(prices / prices.shift(step)) * np.sqrt(horizon)) - 1
return returns.dropna()
def var(portfolio, prices, sort="ascending"):
"""Calculate VaR 1-day for portfolio"""
returns = calculate_returns(prices, 1, sort)
pnl = portfolio.dot(returns.T)
daily_pnl = pnl.sum(axis=0)
daily_pnl_asc = daily_pnl.sort_values(ascending=True)
return 0.4 * daily_pnl_asc[1] + 0.6 * daily_pnl_asc[2]

45
src/var_test.py Normal file
View File

@ -0,0 +1,45 @@
import os
import pandas as pd
import pytest
from pandas.testing import assert_series_equal
from . import var
@pytest.mark.parametrize(
"prices, horizon, ascending, expected",
[
([1, 2, 3, 4, 5], 1, "ascending", [1.0, 0.5, 0.333333, 0.25]),
([5, 4, 3, 2, 1], 1, "descending",[0.25, 0.333333, 0.5, 1.0])
],
)
def test_calculate_returns(prices, horizon, ascending, expected):
assert_series_equal(
pd.Series(var.calculate_returns(pd.DataFrame(prices), horizon, ascending).squeeze()),
pd.Series(expected),
check_names=False,
check_exact=False,
check_index=False,
)
@pytest.mark.parametrize(
"portfolio, prices, expected",
[
({"FX-1": [100.00]}, {"FX-1": [1, 1.1, 0.9, 0.8]}, -6.909090),
({"FX-1": [100.00]}, {"FX-1": [1, 0.9, 0.8, 0.8]}, -10.66666),
],
)
def test_var(portfolio, prices, expected):
assert var.var(pd.DataFrame(portfolio), pd.DataFrame(prices)) == pytest.approx(expected)
def test_end_to_end():
portfolio = pd.DataFrame([{"ccy-1": 153084.81, "ccy-2": 95891.51}])
fx_prices = pd.read_csv(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "var_fx_prices.csv"),
parse_dates=["date"],
index_col="date",
dtype=float,
)
fx_prices.sort_index(inplace=True, ascending=True)
assert var.var(portfolio, fx_prices) == pytest.approx(-13572.73)