├── .github └── workflows │ ├── auto-format-version.yml │ ├── pypi-publish.yml │ └── pytest.yml ├── .gitignore ├── .pylintrc ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── Makefile ├── README.md ├── README.tex.md ├── common.mk ├── data ├── ex1-portfolio.csv └── ex1-stockdata.csv ├── docs ├── Makefile ├── about.rst ├── assets.rst ├── autodoc-examples.sh ├── conf.py ├── developers.rst ├── efficientfrontier.rst ├── examples.rst ├── index.rst ├── license.rst ├── montecarlo.rst ├── movingaverage.rst ├── portfolio.rst ├── quants.rst ├── quickstart.rst └── returns.rst ├── example ├── Example-Analysis.py ├── Example-Build-Portfolio-from-file.py ├── Example-Build-Portfolio-from-web.py ├── Example-Optimisation.py └── Makefile ├── finquant ├── Makefile ├── __init__.py ├── asset.py ├── data_types.py ├── efficient_frontier.py ├── exceptions.py ├── market.py ├── minimise_fun.py ├── monte_carlo.py ├── moving_average.py ├── portfolio.py ├── quants.py ├── returns.py ├── stock.py └── type_utilities.py ├── images ├── bollinger-band.svg ├── cumulative-return.svg ├── ef-mc-overlay.svg ├── finquant-logo-bw.png ├── finquant-logo.png └── ma-band-buysell-signals.svg ├── pyproject.toml ├── requirements.txt ├── requirements_cd.txt ├── requirements_dev.txt ├── requirements_docs.txt ├── requirements_test.txt ├── scripts ├── auto_commit.sh ├── auto_format.sh ├── run_code_analysis.sh ├── update_readme.sh └── update_version.py ├── setup.cfg ├── setup.py ├── tests ├── Makefile ├── test_efficient_frontier.py ├── test_market.py ├── test_moving_average.py ├── test_optimisation.py ├── test_portfolio.py ├── test_quants.py ├── test_returns.py └── test_stock.py ├── tex ├── 27215e5f36fd0308b51ab510444edf0d.svg ├── 738645698dc3073b4bb52a0c078ae829.svg └── ef37c00ad58fe657a64041c3093e0640.svg └── version /.github/workflows/auto-format-version.yml: -------------------------------------------------------------------------------- 1 | name: Formatting and Version Increment 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | env: 9 | SOURCE_BRANCH: ${{ github.head_ref }} 10 | BASE_BRANCH: ${{ github.base_ref }} 11 | 12 | jobs: 13 | code-formatting_increment-version: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.head_ref }} 21 | fetch-depth: 0 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.10' 27 | 28 | - name: Install dependencies 29 | run: | 30 | pip install -r requirements_test.txt 31 | 32 | - name: Run version increment script 33 | id: version_increment 34 | run: | 35 | python scripts/update_version.py ${{ env.BASE_BRANCH }} ${{ env.SOURCE_BRANCH }} 36 | bash scripts/auto_commit.sh "Automated version changes" 37 | continue-on-error: true 38 | 39 | - name: Updating README files 40 | id: update_readme 41 | run: | 42 | bash scripts/update_readme.sh 43 | bash scripts/auto_commit.sh "Updating README files" 44 | continue-on-error: true 45 | 46 | - name: Code formatting and committing changes 47 | id: code_format 48 | run: | 49 | bash scripts/auto_format.sh 50 | bash scripts/auto_commit.sh "Automated formatting changes" 51 | continue-on-error: true 52 | 53 | - name: Push changes to source branch 54 | id: push_to_source_branch 55 | if: ${{ steps.version_increment.outcome == 'success' || steps.update_readme.outcome == 'success' || steps.code_format.outcome == 'success' }} 56 | uses: ad-m/github-push-action@master 57 | with: 58 | branch: ${{ env.SOURCE_BRANCH }} 59 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Python CD - Publish on PyPI 7 | 8 | on: 9 | release: 10 | types: [published] 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | environment: 16 | name: pypi 17 | url: https://pypi.org/p/finquant 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.10' 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | python -m pip install .[cd] 32 | 33 | - name: Build package 34 | run: python setup.py sdist bdist_wheel 35 | 36 | - name: Publish package to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_FINQUANT_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | env: 4 | QUANDLAPIKEY: ${{ secrets.QUANDLAPIKEY }} 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - develop 11 | pull_request: 12 | branches: 13 | - master 14 | - develop 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | python-version: [ '3.10', '3.11' ] 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | python -m pip install .[test] 36 | 37 | - name: Run tests 38 | id: tests 39 | run: make test 40 | continue-on-error: true 41 | 42 | - name: Pylint analysis 43 | id: pylint_analysis 44 | run: | 45 | python -m pylint --fail-under=10 $(git ls-files '*.py') 46 | continue-on-error: true 47 | 48 | - name: mypy analysis 49 | id: mypy_analysis 50 | run: | 51 | python -m mypy *.py finquant 52 | continue-on-error: true 53 | 54 | - name: Check for Failures 55 | run: | 56 | if [[ "${{ steps.tests.outcome }}" != "success" || "${{ steps.pylint_analysis.outcome }}" != "success" || "${{ steps.mypy_analysis.outcome }}" != "success" ]]; then 57 | echo "Pipeline failed due to errors in the following steps:" 58 | echo "Tests: ${{ steps.tests.outcome }}" 59 | echo "Pylint: ${{ steps.pylint_analysis.outcome }}" 60 | echo "mypy: ${{ steps.mypy_analysis.outcome }}" 61 | exit 1 62 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################ 2 | # Python files # 3 | ################ 4 | *.pyc 5 | *.ipynb 6 | .ipynb_checkpoints 7 | 8 | ####################### 9 | # Documentation files # 10 | ####################### 11 | docs/build/* 12 | docs/auto-Example-*.py 13 | 14 | ############### 15 | # Build files # 16 | ############### 17 | build/* 18 | FinQuant.egg-info/* 19 | 20 | ################## 21 | # Text artifacts # 22 | ################## 23 | *~ 24 | *.swp 25 | 26 | ############## 27 | # bkup files # 28 | ############## 29 | **/bkup-files 30 | 31 | ############### 32 | # latex files # 33 | ############### 34 | *.log 35 | *.aux 36 | *.out 37 | *.pdf 38 | 39 | ######### 40 | # other # 41 | ######### 42 | .idea -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Ignore certain files or directories during analysis 3 | ignore-paths=tests/,docs/,example/ 4 | extension-pkg-whitelist=pydantic 5 | 6 | [REPORTS] 7 | # Set the output format for `pylint` messages (text, colorized, json, etc.) 8 | output-format=text 9 | 10 | [MESSAGES CONTROL] 11 | # Specify the maximum allowed line length 12 | max-line-length=120 13 | 14 | # Disable some pylint messages that may be too strict or not relevant for your project 15 | disable= 16 | C0114, # Missing module docstring 17 | C0116, # Missing function docstring 18 | R0902, # Too many instance attributes 19 | R0903, # Too few public methods 20 | R0913, # Too many arguments 21 | R0914, # Too many local variables 22 | R1705, # Unnecessary "else" after "return" 23 | W1514, # Unspecified encoding, 24 | 25 | # Include additional pylint messages or message categories 26 | #enable= 27 | # C0114, # Missing module, function, class docstring 28 | # R0903, # Too few public methods 29 | 30 | [FORMAT] 31 | good-names = pf, df, ef, mc, mu 32 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - test 19 | - docs 20 | 21 | # Build documentation in the docs/ directory with Sphinx 22 | sphinx: 23 | configuration: docs/conf.py -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to FinQuant 2 | First of all, thank you for your interest in FinQuant and wanting to contribute. Your help is much appreciated. 3 | 4 | Here are some guidelines for contributing to FinQuant. 5 | 6 | ## Reporting Bugs and Issues 7 | Before creating a new issue/bug report, check the list of existing [issues](https://github.com/fmilthaler/FinQuant/issues). When raising a new [issue](https://github.com/fmilthaler/FinQuant/issues), include as many details as possible and follow the following guidelines: 8 | - Use a clear and descriptive title for the issue to identify the problem. 9 | - Provide a minimal example, a series of steps to reproduce the problem. 10 | - Describe the behaviour you observed after following the steps. 11 | - Explain which behaviour you expected to see instead and why. 12 | - State versions of FinQuant/Python and Operating system you are using. 13 | - Provide error messages if applicable. 14 | 15 | ## Coding Guidelines 16 | So you wish to fix bugs yourself, or contribute by adding a new feature. Awesome! Here are a few guidelines I would like you to respect. 17 | 18 | ### Create a fork 19 | First off, you should create a fork. Within your fork, create a new branch. Depending on what you want to do, 20 | choose one of the following prefixes for your branch name: 21 | - `bugfix/` followed by something like ``: to be used for bug fixes 22 | - `feature/` followed by something like ``: to be used for adding a new feature 23 | 24 | If you simply want to refactor the code base, or do other types of chores, use one of the following branch name prefixes: 25 | - `refactor/` followed by something like `` 26 | - `chore/` followed by something like `` 27 | 28 | **NOTE**: It is _mandatory_ to use one of the above prefixes for your branch name. FinQuant uses GitHub workflows 29 | to automatically bump the version number when a PR is merged into `master` (or `develop`). 30 | The new version number depends on the source branch name of the merged PR. 31 | 32 | Example: 33 | . If you are working on a bugfix to fix a print statement of the portfolio properties, 34 | your branch name should be something like bugfix/print-statement-portfolio-properties. 35 | For the automated versioning to work, the branch name is required to start with `bugfix/` or one of the other 36 | above mentioned patterns. 37 | 38 | ### Custom data types 39 | [FinQuant defines a number of custom data types](https://finquant.readthedocs.io/en/latest/developers.html#data-types) 40 | in the module `finquant.data_types`. 41 | 42 | These data types are useful as lots of functions/methods in FinQuant allow arguments to be of different data types. 43 | For example: 44 | - `data` is often accepted as either a `pandas.Series` or `pandas.DataFrame`, or 45 | - `risk_free_rate` could be a Python `float` or a `numpy.float64` among others. 46 | 47 | To accommodate and simplify this, custom data types are defined in the module `finquant.data_types`. 48 | Please familiarize yourself with those and add more if your code requires them. 49 | 50 | ### Data type validation 51 | [FinQuant provides a module/function for type validation](https://finquant.readthedocs.io/en/latest/developers.html#type-validation), 52 | which is used throughout the code base for type validation purposes. Said function simplifies checking an argument 53 | against its expected type and reduces the amount of copy-pasted `if` and `raise` statements. 54 | You can check out the source code in `finquant.type_utilities`. 55 | 56 | ### Commit your changes 57 | Make your changes to the code, and write sensible commit messages. 58 | 59 | ### Tests 60 | In the root directory of your version of FinQuant, run `make test` and make sure all tests are passing. 61 | If applicable, add new tests in the `./tests/` directory. Tests should be written with `pytest`. 62 | 63 | Some few tests require you to have a [Quandl API key](https://docs.quandl.com/docs#section-authentication). 64 | If you do not have one locally, you can ignore the tests that are failing due to a missing Quandl API key. 65 | Once you open a PR, all tests are run by GitHub Actions with a pre-configured key. 66 | 67 | ### Documentation 68 | If applicable, please add docstrings to new functions/classes/modules. 69 | Follow example of existing docstrings. FinQuant uses `sphinx` to generate Documentation 70 | for [ReadTheDocs](https://finquant.readthedocs.io) automatically from docstrings. 71 | 72 | ### Style 73 | Fortunately for you, you can ignore code formatting and fully focus on your contribution. 74 | FinQuant uses a GitHub workflow that is automatically triggered and runs [Black](https://github.com/psf/black) and 75 | [isort](https://pycqa.github.io/isort/) to format the code base for you. 76 | 77 | ### Create a Pull Request 78 | Create a new [Pull Request](https://github.com/fmilthaler/FinQuant/pulls). 79 | Describe what your changes are in the Pull Request. 80 | If your contribution fixes a bug, or adds a features listed under 81 | [issues](https://github.com/fmilthaler/FinQuant/issues) as "#12", please add "fixes #12" or "closes #12". 82 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Thank you to all the individuals who have contributed to this project! 4 | 5 | *Please note: The order of contributors does not imply the significance or quantity of their contributions.* 6 | 7 | ## Maintainers 8 | 9 | - Frank Milthaler (@fmilthaler): Original author 10 | - Pietropaolo Frisoni (@PietropaoloFrisoni): Current maintainer of FinQuant, helped resurrect the project, added new features, code review, bug fixing 11 | 12 | ## Contributors 13 | 14 | - Richard Stromer (@noxan): bug fixing code snippet in README.md 15 | - Stephen Pennington (@slpenn13): bug fixing pandas MultiIndex usage 16 | - @herrfz: bug fix for selecting stock data from portfolio, handling NaN values and type checks 17 | - @drcsturm: bug fix for single stock portfolio 18 | - @donin1129: bug fix for pandas index reference 19 | - @aft90: helped to implement the Sortino Ratio 20 | - David Cheeseman (@nuvious): added `defer_update` flag to `add_stock` function so bulk adding of stocks can have update deferred until after all are added (improved performance). 21 | 22 | ## Special Thanks 23 | 24 | We would also like to acknowledge the following individuals for their valuable contributions, support and interest: 25 | 26 | - Pietropaolo Frisoni (@PietropaoloFrisoni): Many thanks for helping me to resurrect FinQuant in 2023 27 | - @Leohanhart: Thank you for bringing several issues to my attention and for providing detailed information. 28 | 29 | Finally, many thanks to all users of FinQuant. :) 30 | 31 | --- 32 | 33 | If you have contributed to this project and your name is missing, please submit a pull request to add your details to this file. 34 | 35 | Thank you for making this project better! -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 Frank Milthaler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include common.mk 2 | 3 | PYDIR=finquant 4 | DATADIR=data 5 | EXAMPLEDIR=example 6 | EXAMPLEFILES=$(wildcard example/Example*.py) 7 | TESTDIR=tests 8 | DOCDIR=docs 9 | DISTDIR=dist 10 | BUILDDIR=build 11 | AUTODOCEXAMPLES=autodoc-examples.sh 12 | CLEANDIRS = $(PYDIR:%=clean-%) \ 13 | $(EXAMPLEDIR:%=clean-%) \ 14 | $(TESTDIR:%=clean-%) \ 15 | $(DOCDIR:%=clean-%) 16 | 17 | SEARCH= 18 | 19 | .PHONY: test 20 | .PHONY: doc 21 | .PHONY: EXAMPLEFILES $(EXAMPLEFILES) 22 | .PHONY: cleandirs $(CLEANDIRS) 23 | .PHONY: clean 24 | .PHONY: dirclean 25 | 26 | all: clean 27 | 28 | test:copyexamples 29 | @echo "Running tests" 30 | @$(MAKE) -C $(TESTDIR) 31 | 32 | copyexamples: $(EXAMPLEFILES) 33 | $(EXAMPLEFILES): 34 | @cp $(@) $(subst example/,tests/test_,$(@)) 35 | 36 | pypi: 37 | @$(PYTHON) setup.py sdist bdist_wheel 38 | @$(PYTHON) -m twine upload dist/FinQuant-* 39 | 40 | doc: 41 | @$(MAKE) -C $(DOCDIR) clean 42 | @$(MAKE) -C $(DOCDIR) html 43 | 44 | clean: dirclean 45 | clean: 46 | -@rm -rf .pytest_cache FinQuant.egg-info $(BUILDDIR)/ $(DISTDIR)/ 47 | 48 | dirclean: $(CLEANDIRS) 49 | $(CLEANDIRS): 50 | @echo "cleaning directory $(@:clean-%=%):" 51 | -@$(MAKE) -C $(@:clean-%=%) clean 52 | 53 | search: 54 | @echo "searching all python files for $(SEARCH):" 55 | @find . \( -name "*.py" -o -name "README.tex.md" \) -not -path "./*/bkup-files/*" -not -path "./docs/build/*" -not -path "./build/*" | xargs grep -i --color=auto $(SEARCH) 56 | 57 | -------------------------------------------------------------------------------- /README.tex.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | pypi 8 | 9 | 10 | pypi 11 | 12 | 13 | GitHub Actions 14 | 15 | 16 | docs 17 | 18 | 19 | contributors 20 | 21 | 22 | contributions 23 | 24 | 25 | license 26 | 27 |

28 | 29 | # FinQuant 30 | *FinQuant* is a program for financial **portfolio management, analysis and optimisation**. 31 | 32 | This README only gives a brief overview of *FinQuant*. The interested reader should refer to its [documentation](https://finquant.readthedocs.io "FinQuant Documentation"). 33 | 34 | ## Table of contents 35 | - [Motivation](#Motivation) 36 | - [Installation](#Installation) 37 | - [Portfolio Management](#Portfolio-Management) 38 | - [Returns](#Returns) 39 | - [Moving Averages](#Moving-Averages) 40 | - [Portfolio Optimisation](#Portfolio-Optimisation) 41 | - [Efficient Frontier](#Efficient-Frontier) 42 | - [Monte Carlo](#Monte-Carlo) 43 | - [Examples](#Examples) 44 | - [Building a portfolio with data from web](#Building-a-portfolio-with-data-from-web) 45 | - [Building a portfolio with preset data](#Building-a-portfolio-with-preset-data) 46 | - [Analysis of a portfolio](#Analysis-of-a-portfolio) 47 | - [Optimisation of a portfolio](#Optimisation-of-a-portfolio) 48 | 49 | ## Motivation 50 | Within a few lines of code, *FinQuant* can generate an object that holds your stock prices of your desired financial portfolio, analyses it, and can create plots of different kinds of *Returns*, *Moving Averages*, *Moving Average Bands with buy/sell signals*, and *Bollinger Bands*. It also allows for the optimisation based on the *Efficient Frontier* or a *Monte Carlo* run of the financial portfolio within a few lines of code. Some of the results are shown here. 51 | 52 | ### Automatically generating an instance of `Portfolio` 53 | `finquant.portfolio.build_portfolio` is a function that eases the creating of your portfolio. See below for one of several ways of using `build_portfolio`. 54 | ``` 55 | from finquant.portfolio import build_portfolio 56 | names = ['GOOG', 'AMZN', 'MCD', 'DIS'] 57 | start_date = '2015-01-01' 58 | end_date = '2017-12-31' 59 | pf = build_portfolio(names=names, 60 | start_date=start_date, 61 | end_date=end_date) 62 | ``` 63 | `pf` is an instance of `finquant.portfolio.Portfolio`, which contains the prices of the stocks in your portfolio. Then... 64 | ``` 65 | pf.data.head(3) 66 | ``` 67 | yields 68 | ``` 69 | GOOG AMZN MCD DIS 70 | Date 71 | 2015-01-02 524.81 308.52 85.783317 90.586146 72 | 2015-01-05 513.87 302.19 84.835892 89.262380 73 | 2015-01-06 501.96 295.29 84.992263 88.788916 74 | ``` 75 | 76 | ### Portfolio properties 77 | Nicely printing out the portfolio's properties 78 | ``` 79 | pf.properties() 80 | ``` 81 | Depending on the stocks within your portfolio, the output looks something like the below. 82 | ``` 83 | ---------------------------------------------------------------------- 84 | Stocks: GOOG, AMZN, MCD, DIS 85 | Time window/frequency: 252 86 | Risk free rate: 0.005 87 | Portfolio expected return: 0.266 88 | Portfolio volatility: 0.156 89 | Portfolio Sharpe ratio: 1.674 90 | 91 | Skewness: 92 | GOOG AMZN MCD DIS 93 | 0 0.124184 0.087516 0.58698 0.040569 94 | 95 | Kurtosis: 96 | GOOG AMZN MCD DIS 97 | 0 -0.751818 -0.856101 -0.602008 -0.892666 98 | 99 | Information: 100 | Allocation Name 101 | 0 0.25 GOOG 102 | 1 0.25 AMZN 103 | 2 0.25 MCD 104 | 3 0.25 DIS 105 | ---------------------------------------------------------------------- 106 | ``` 107 | 108 | ### Cumulative Return 109 | ``` 110 | pf.comp_cumulative_returns().plot().axhline(y = 0, color = "black", lw = 3) 111 | ``` 112 | yields 113 |

114 | 115 |

116 | 117 | ### Band Moving Average (Buy/Sell Signals) 118 | ``` 119 | from finquant.moving_average import compute_ma, ema 120 | # get stock data for disney 121 | dis = pf.get_stock("DIS").data.copy(deep=True) 122 | spans = [10, 50, 100, 150, 200] 123 | ma = compute_ma(dis, ema, spans, plot=True) 124 | ``` 125 | yields 126 |

127 | 128 |

129 | 130 | ### Bollinger Band 131 | ``` 132 | from finquant.moving_average import plot_bollinger_band, sma 133 | # get stock data for disney 134 | dis = pf.get_stock("DIS").data.copy(deep=True) 135 | span=20 136 | plot_bollinger_band(dis, sma, span) 137 | ``` 138 | yields 139 |

140 | 141 |

142 | 143 | ### Portfolio Optimisation 144 | ``` 145 | # performs and plots results of Monte Carlo run (5000 iterations) 146 | opt_w, opt_res = pf.mc_optimisation(num_trials=5000) 147 | # plots the results of the Monte Carlo optimisation 148 | pf.mc_plot_results() 149 | # plots the Efficient Frontier 150 | pf.ef_plot_efrontier() 151 | # plots optimal portfolios based on Efficient Frontier 152 | pf.ef.plot_optimal_portfolios() 153 | # plots individual plots of the portfolio 154 | pf.plot_stocks() 155 | ``` 156 |

157 | 158 |

159 | 160 | ## Installation 161 | As it is common for open-source projects, there are several ways to get hold of the code. Choose whichever suits you and your purposes best. 162 | 163 | ### Dependencies 164 | *FinQuant* depends on the following Python packages: 165 | - python>=3.10 166 | - numpy>=1.15 167 | - pandas>=2.0 168 | - matplotlib>=3.0 169 | - quandl>=3.4.5 170 | - yfinance>=0.1.43 171 | - scipy>=1.2.0 172 | - scikit-learn>=1.3.0 173 | 174 | ### From PyPI 175 | *FinQuant* can be obtained from PyPI 176 | 177 | ```pip install FinQuant``` 178 | 179 | ### From GitHub 180 | Get the code from GitHub: 181 | 182 | ```git clone https://github.com/fmilthaler/FinQuant.git``` 183 | 184 | Then inside `FinQuant` run: 185 | 186 | ```python setup.py install``` 187 | 188 | Alternatively, if you do not wish to install *FinQuant*, you can also download/clone it as stated above, and then make sure to add it to your ``PYTHONPATH``. 189 | 190 | ## Portfolio Management 191 | This is the core of *FinQuant*. `finquant.portfolio.Portfolio` provides an object that holds prices of all stocks in your portfolio, and automatically computes the most common quantities for you. To make *FinQuant* an user-friendly program, that combines data analysis, visualisation and optimisation, the object provides interfaces to the main features that are provided in the modules in `./finquant/`. 192 | 193 | To learn more about the object, please read through the [documentation](https://finquant.readthedocs.io/en/latest/ "FinQuant Documentation"), docstring of the module/class, and/or have a look at the examples. 194 | 195 | `finquant.portfolio.Portfolio` also provides a function `build_portfolio` which is designed to automatically generate an instance of `Portfolio` for the user's convenience. For more information on how to use `build_portfolio`, please refer to the [documentation](https://finquant.readthedocs.io/en/latest/ "FinQuant Documentation"), its `docstring` and/or have a look at the examples. 196 | 197 | ## Returns 198 | Daily returns of stocks are often computed in different ways. *FinQuant* provides three different ways of computing the daily returns in `finquant.returns`: 199 | 1. The cumulative return: $\displaystyle\dfrac{\text{price}_{t_i} - \text{price}_{t_0} + \text{dividend}}{\text{price}_{t_0}}$ 200 | 2. Percentage change of daily returns: $\displaystyle\dfrac{\text{price}_{t_i} - \text{price}_{t_{i-1}}}{\text{price}_{t_{i-1}}}$ 201 | 3. Log Return: $\displaystyle\log\left(1 + \dfrac{\text{price}_{t_i} - \text{price}_{t_{i-1}}}{\text{price}_{t_{i-1}}}\right)$ 202 | 203 | In addition to those, the module provides the function `historical_mean_return(data, freq=252)`, which computes the historical mean of the daily returns over a time period `freq`. 204 | 205 | ## Moving Averages 206 | The module `finquant.moving_average` allows the computation and visualisation of Moving Averages of the stocks listed in the portfolio is also provided. It entails functions to compute and visualise the 207 | - `sma`: Simple Moving Average, and 208 | - `ema`: Exponential Moving Average. 209 | - `compute_ma`: a Band of Moving Averages (of different time windows/spans) including Buy/Sell signals 210 | - `plot_bollinger_band`: a Bollinger Band for 211 | - `sma`, 212 | - `ema`. 213 | 214 | ## Portfolio Optimisation 215 | ### Efficient Frontier 216 | An implementation of the Efficient Frontier (`finquant.efficient_frontier.EfficientFrontier`) allows for the optimisation of the portfolio for 217 | - `minimum_volatility` Minimum Volatility, 218 | - `maximum_sharpe_ratio` Maximum Sharpe Ratio 219 | - `efficient_return` Minimum Volatility for a given expected return 220 | - `efficient_volatility` Maximum Sharpe Ratio for a given target volatility 221 | 222 | by performing a numerical solve to minimise/maximise an objective function. 223 | 224 | Often it is useful to visualise the *Efficient Frontier* as well as the optimal solution. This can be achieved with the following methods: 225 | - `plot_efrontier`: Plots the *Efficient Frontier*. If no minimum/maximum Return values are provided, the algorithm automatically chooses those limits for the *Efficient Frontier* based on the minimum/maximum Return values of all stocks within the given portfolio. 226 | - `plot_optimal_portfolios`: Plots markers of the portfolios with the Minimum Volatility and Maximum Sharpe Ratio. 227 | 228 | For reasons of user-friendliness, interfaces to these functions are provided in `finquant.portfolio.Portfolio`. Please have a look at the [documentation](https://finquant.readthedocs.io "FinQuant Documentation"). 229 | 230 | ### Monte Carlo 231 | Alternatively a *Monte Carlo* run of `n` trials can be performed to find the optimal portfolios for 232 | - minimum volatility, 233 | - maximum Sharpe ratio 234 | 235 | The approach branded as *Efficient Frontier* should be the preferred method for reasons of computational effort and accuracy. The latter approach is only included for the sake of completeness, and creation of beautiful plots. 236 | 237 | ## Examples 238 | For more information about the project and details on how to use it, please 239 | look at the examples provided in `./example`. 240 | 241 | **Note**: In the below examples, `pf` refers to an instance of `finquant.portfolio.Portfolio`, the object that holds all stock prices and computes its most common quantities automatically. To make *FinQuant* a user-friendly program, that combines data analysis, visualisation and optimisation, the object also provides interfaces to the main features that are provided in the modules in `./finquant/` and are discussed throughout this README. 242 | 243 | ### Building a portfolio with data from web 244 | `./example/Example-Build-Portfolio-from-web.py`: Shows how to use *FinQuant* to build a financial portfolio by downloading stock price data through the Python package `quandl`/`yfinance`. 245 | 246 | ### Building a portfolio with preset data 247 | `./example/Example-Build-Portfolio-from-file.py`: Shows how to use *FinQuant* to build a financial portfolio by providing stock price data yourself, e.g. by reading data from disk/file. 248 | 249 | ### Analysis of a portfolio 250 | `./example/Example-Analysis.py`: This example shows how to use an instance of `finquant.portfolio.Portfolio`, get the portfolio's quantities, such as 251 | - Expected Returns, 252 | - Volatility, 253 | - Downside Risk, 254 | - Value at Risk, 255 | - Sharpe Ratio, 256 | - Sortino Ratio, 257 | - Treynor Ratio, 258 | - Beta parameter, 259 | - R squared coefficient. 260 | 261 | It also shows how to extract individual stocks from the given portfolio. Moreover it shows how to compute and visualise: 262 | - the different Returns provided by the module `finquant.returns`, 263 | - *Moving Averages*, a band of *Moving Averages*, and a *Bollinger Band*. 264 | 265 | ### Optimisation of a portfolio 266 | `./example/Example-Optimisation.py`: This example focusses on the optimisation of a portfolio. To achieve this, the example shows the usage of `finquant.efficient_frontier.EfficientFrontier` for optimising the portfolio, for the 267 | - Minimum Volatility 268 | - Maximum Sharpe Ratio 269 | - Minimum Volatility for a given target Return 270 | - Maximum Sharpe Ratio for a given target Volatility. 271 | 272 | Furthermore, it is also shown how the entire *Efficient Frontier* and the optimal portfolios can be computed and visualised. If needed, it also gives an example of plotting the individual stocks of the given portfolio within the computed *Efficient Frontier*. 273 | 274 | Also, the optimisation of a portfolio and its visualisation based on a *Monte Carlo* is shown. 275 | 276 | Finally, *FinQuant*'s visualisation methods allow for overlays, if this is desired. Thus, with only the following few lines of code, one can create an overlay of the *Monte Carlo* run, the *Efficient Frontier*, its optimised portfolios for *Minimum Volatility* and *Maximum Sharpe Ratio*, as well as the portfolio's individual stocks. 277 | -------------------------------------------------------------------------------- /common.mk: -------------------------------------------------------------------------------- 1 | PYTHON=python3 2 | -------------------------------------------------------------------------------- /data/ex1-portfolio.csv: -------------------------------------------------------------------------------- 1 | Allocation,Name 2 | 20.0,WIKI/GOOG 3 | 10.0,WIKI/AMZN 4 | 15.0,WIKI/MCD 5 | 18.0,WIKI/DIS 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = ./ 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | 21 | clean: 22 | -@rm -rf $(BUILDDIR) 23 | -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | .. _about: 2 | 3 | ##### 4 | About 5 | ##### 6 | 7 | I was inspired to develop this program by recent application procedures in the finance sector. Being tasked with a typical exercise for a *Quant* position at an investment firm, I first started doing some reading about finance and financial portfolios in particular. Successfully completing the exercise felt good, but I had many more ideas on how to improve and extend my solution. The result of which is *FinQuant*. 8 | 9 | My academical background is quite diverse, as I studied Mechanical Engineering & Computer Science, Systems & Control Engineering for my undergraduate and postgraduate courses respectively, followed by a doctorate in Computational Physics at Imperial College London. With that skillset I decided to go rogue and enter the field of Data Science/Engineering. 10 | 11 | I enjoy challenging myself and learning new things, which is probably the reason for me working on this and other projects. 12 | -------------------------------------------------------------------------------- /docs/assets.rst: -------------------------------------------------------------------------------- 1 | .. _assets: 2 | 3 | ################# 4 | Individual Assets 5 | ################# 6 | FinQuant provides classes for individual assets, such as stocks or funds. These are explained below. 7 | 8 | Asset 9 | ===== 10 | .. automodule:: finquant.asset 11 | .. autoclass:: finquant.asset.Asset 12 | :members: 13 | 14 | .. automethod:: __init__ 15 | 16 | 17 | Stock 18 | ===== 19 | Inherits from ``Asset``. 20 | 21 | .. automodule:: finquant.stock 22 | .. autoclass:: finquant.stock.Stock 23 | :members: 24 | 25 | .. automethod:: __init__ 26 | 27 | 28 | Market 29 | ====== 30 | Inherits from ``Asset``. 31 | 32 | .. automodule:: finquant.market 33 | .. autoclass:: finquant.market.Market 34 | :members: 35 | 36 | .. automethod:: __init__ 37 | -------------------------------------------------------------------------------- /docs/autodoc-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # copy example files 4 | for i in ../example/Example*.py 5 | do 6 | cp $i ./auto-${i##*example/} 7 | done 8 | 9 | # remove jupyter notebook information from py files: 10 | for i in auto-Example*.py 11 | do 12 | sed -i '/-*- coding: utf-8 -*-/d' $i 13 | sed -i '/4/{ N; d; }' $i 14 | sed -i '//{ N; d; }' $i 15 | sed -i '//{ N; d; }' $i 16 | # also removing plotting styles to keep code snippet shortish: 17 | sed -i '/plotting style:/d' $i 18 | sed -i '/plt.style.use/{ N; d; }' $i 19 | sed -i '/plt.rcParams/{ N; d; }' $i 20 | done 21 | 22 | echo "+++++++++++++++++++++++++++++++++++++++++++++++++++++" 23 | echo "Example files were successfully copied and processed." 24 | echo "+++++++++++++++++++++++++++++++++++++++++++++++++++++" 25 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath("..")) 19 | 20 | # run bash script for examples 21 | os.system("./autodoc-examples.sh") 22 | 23 | # get version/release from file 24 | with open("../version", "r") as f: 25 | ver = dict(x.rstrip().split("=") for x in f) 26 | 27 | # -- Project information ----------------------------------------------------- 28 | 29 | project = "FinQuant" 30 | copyright = "2019, Frank Milthaler" 31 | author = "Frank Milthaler" 32 | 33 | # The short X.Y version 34 | version = ver["version"] 35 | # The full version, including alpha/beta/rc tags 36 | release = ver["release"] 37 | 38 | # -- General configuration --------------------------------------------------- 39 | 40 | # If your documentation needs a minimal Sphinx version, state it here. 41 | # 42 | # needs_sphinx = '1.0' 43 | 44 | # Add any Sphinx extension module names here, as strings. They can be 45 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 46 | # ones. 47 | extensions = [ 48 | "sphinx.ext.autodoc", 49 | "sphinx.ext.githubpages", 50 | "sphinx_autodoc_typehints", 51 | ] 52 | 53 | # Make sure the 'members' flag is included 54 | autodoc_default_flags = ["members"] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ["ntemplates"] 58 | 59 | # The suffix(es) of source filenames. 60 | # You can specify multiple suffix as a list of string: 61 | # 62 | # source_suffix = ['.rst', '.md'] 63 | source_suffix = ".rst" 64 | 65 | # The master toctree document. 66 | master_doc = "index" 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = "en" 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | # This pattern also affects html_static_path and html_extra_path. 78 | exclude_patterns = [] 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = "sphinx" 82 | 83 | 84 | # -- Options for HTML output ------------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = "sphinx_rtd_theme" 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | # html_theme_options = {} 96 | 97 | # Add any paths that contain custom static files (such as style sheets) here, 98 | # relative to this directory. They are copied after the builtin static files, 99 | # so a file named "default.css" will overwrite the builtin "default.css". 100 | html_static_path = [] 101 | 102 | # Custom sidebar templates, must be a dictionary that maps document names 103 | # to template names. 104 | # 105 | # The default sidebars (for documents that don't match any pattern) are 106 | # defined by theme itself. Builtin themes are using these templates by 107 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 108 | # 'searchbox.html']``. 109 | # 110 | # html_sidebars = {} 111 | 112 | 113 | # -- Options for HTMLHelp output --------------------------------------------- 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = "FinQuantdoc" 117 | 118 | 119 | # -- Options for LaTeX output ------------------------------------------------ 120 | 121 | latex_elements = { 122 | # The paper size ('letterpaper' or 'a4paper'). 123 | # 124 | # 'papersize': 'letterpaper', 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | # Additional stuff for the LaTeX preamble. 129 | # 130 | # 'preamble': '', 131 | # Latex figure (float) alignment 132 | # 133 | # 'figure_align': 'htbp', 134 | } 135 | 136 | # Grouping the document tree into LaTeX files. List of tuples 137 | # (source start file, target name, title, 138 | # author, documentclass [howto, manual, or own class]). 139 | latex_documents = [ 140 | (master_doc, "FinQuant.tex", "FinQuant Documentation", "Frank Milthaler", "manual") 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [(master_doc, "finquant", "FinQuant Documentation", [author], 1)] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | ( 158 | master_doc, 159 | "FinQuant", 160 | "FinQuant Documentation", 161 | author, 162 | "FinQuant", 163 | "One line description of project.", 164 | "Miscellaneous", 165 | ) 166 | ] 167 | 168 | 169 | # -- Options for Epub output ------------------------------------------------- 170 | 171 | # Bibliographic Dublin Core info. 172 | epub_title = project 173 | 174 | # The unique identifier of the text. This can be a ISBN number 175 | # or the project homepage. 176 | # 177 | # epub_identifier = '' 178 | 179 | # A unique identification for the text. 180 | # 181 | # epub_uid = '' 182 | 183 | # A list of files that should not be packed into the epub file. 184 | epub_exclude_files = ["search.html"] 185 | 186 | 187 | # -- Extension configuration ------------------------------------------------- 188 | -------------------------------------------------------------------------------- /docs/developers.rst: -------------------------------------------------------------------------------- 1 | .. _developers: 2 | 3 | #################### 4 | Notes for Developers 5 | #################### 6 | 7 | .. note:: Contributions are welcome. If you want to add new functionality please 8 | 9 | 1. read through `CONTRIBUTIONS.md` in the root directory of the repository, and 10 | 2. familiarize yourself with the custom data types defined in FinQuant, and how type validation is achieved. You find relevant information below. 11 | 12 | ********** 13 | Data Types 14 | ********** 15 | 16 | Various custom data types are defined in ``finquant.data_types`` and used in FinQuant as type hints. 17 | 18 | Description 19 | ########### 20 | 21 | .. automodule:: finquant.data_types 22 | 23 | 24 | 25 | Code Definitions 26 | ################ 27 | 28 | Array/List-Like Types 29 | --------------------- 30 | 31 | .. autodata:: finquant.data_types.ARRAY_OR_LIST 32 | :annotation: 33 | 34 | .. autodata:: finquant.data_types.ARRAY_OR_DATAFRAME 35 | :annotation: 36 | 37 | .. autodata:: finquant.data_types.ARRAY_OR_SERIES 38 | :annotation: 39 | 40 | .. autodata:: finquant.data_types.SERIES_OR_DATAFRAME 41 | :annotation: 42 | 43 | List of Dict keys 44 | ----------------- 45 | 46 | .. autodata:: finquant.data_types.LIST_DICT_KEYS 47 | :annotation: 48 | 49 | Numeric Types 50 | ------------- 51 | 52 | .. autodata:: finquant.data_types.FLOAT 53 | :annotation: 54 | 55 | .. autodata:: finquant.data_types.INT 56 | :annotation: 57 | 58 | .. autodata:: finquant.data_types.NUMERIC 59 | :annotation: 60 | 61 | 62 | *************** 63 | Type validation 64 | *************** 65 | 66 | This module provides a function ``type_validation`` that allow to effortlessly implement type validation. 67 | 68 | Description 69 | ########### 70 | 71 | .. automodule:: finquant.type_utilities 72 | 73 | 74 | Code Definitions 75 | ################ 76 | 77 | .. autodata:: finquant.type_utilities.type_validation 78 | :annotation: 79 | -------------------------------------------------------------------------------- /docs/efficientfrontier.rst: -------------------------------------------------------------------------------- 1 | .. _efficientfrontier: 2 | 3 | ################## 4 | Efficient Frontier 5 | ################## 6 | 7 | *FinQuant* allows to optimise a given portfolio by minimising a cost/objective function. The module ``finquant.efficient_frontier`` contains a class ``EfficientFrontier`` that provides public functions to compute and visualise optimised portfolios. 8 | 9 | .. autoclass:: finquant.efficient_frontier.EfficientFrontier 10 | :members: 11 | 12 | .. automethod:: __init__ 13 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _GitHub: https://github.com/fmilthaler/FinQuant/ 2 | 3 | .. _examples: 4 | 5 | ######## 6 | Examples 7 | ######## 8 | 9 | 10 | For more information about the project and details on how to use it, please 11 | look at the examples discussed below. 12 | 13 | .. note:: In the below examples, ``pf`` refers to an instance of ``finquant.portfolio.Portfolio``, the object that holds all stock prices and computes its most common quantities automatically. To make *FinQuant* a user-friendly program, that combines data analysis, visualisation and optimisation, the object also provides interfaces to the main features that are provided in the modules in ``./finquant/`` and are discussed throughout this documentation. 14 | 15 | 16 | Building a portfolio with data from web *quandl*/*yfinance* 17 | =========================================================== 18 | This example shows how to use *FinQuant* to build a financial portfolio by downloading stock price data by using the Python package *quandl*/*yfinance*. 19 | 20 | .. note:: This example refers to ``example/Example-Build-Portfolio-from-web.py`` of the `GitHub`_ repository. It can be downloaded with jupyter notebook cell information: :download:`download Example-Build-Portfolio-from-web.py <../example/Example-Build-Portfolio-from-web.py>` 21 | 22 | .. literalinclude:: ./auto-Example-Build-Portfolio-from-web.py 23 | :linenos: 24 | :language: python 25 | 26 | 27 | Building a portfolio with preset data 28 | ===================================== 29 | This example shows how to use *FinQuant* to build a financial portfolio by providing stock price data yourself, e.g. by reading data from disk/file. 30 | 31 | .. note:: This example refers to ``example/Example-Build-Portfolio-from-file.py`` of the `GitHub`_ repository. It can be downloaded with jupyter notebook cell information: :download:`download Example-Build-Portfolio-from-file.py <../example/Example-Build-Portfolio-from-file.py>` 32 | 33 | .. literalinclude:: ./auto-Example-Build-Portfolio-from-file.py 34 | :linenos: 35 | :language: python 36 | 37 | 38 | Analysis of a portfolio 39 | ======================= 40 | This example shows how to use an instance of ``finquant.portfolio.Portfolio``, get the portfolio's quantities, such as 41 | 42 | - Expected Returns, 43 | - Volatility, 44 | - Sharpe Ratio. 45 | 46 | It also shows how to extract individual stocks from the given portfolio. Moreover it shows how to compute and visualise: 47 | 48 | - the different Returns provided by the module ``finquant.returns``, 49 | - *Moving Averages*, a band of *Moving Averages*, and a *Bollinger Band*. 50 | 51 | .. note:: This example refers to ``example/Example-Analysis.py`` of the `GitHub`_ repository. It can be downloaded with jupyter notebook cell information: :download:`download Example-Analysis.py <../example/Example-Analysis.py>` 52 | 53 | .. literalinclude:: ./auto-Example-Analysis.py 54 | :linenos: 55 | :language: python 56 | 57 | 58 | Optimisation of a portfolio 59 | =========================== 60 | This example focusses on the optimisation of a portfolio. To achieve this, the example shows the usage of ``finquant.efficient_frontier.EfficientFrontier`` for numerically optimising the portfolio, for the 61 | 62 | - Minimum Volatility 63 | - Maximum Sharpe Ratio 64 | - Minimum Volatility for a given target Return 65 | - Maximum Sharpe Ratio for a given target Volatility. 66 | 67 | Furthermore, it is also shown how the entire *Efficient Frontier* and the optimal portfolios can be computed and visualised. If needed, it also gives an example of plotting the individual stocks of the given portfolio within the computed *Efficient Frontier*. 68 | 69 | Also, the optimisation of a portfolio and its visualisation based on a *Monte Carlo* is shown. 70 | 71 | Finally, *FinQuant*'s visualisation methods allow for overlays, if this is desired. Thus, with only the following few lines of code, one can create an overlay of the *Monte Carlo* run, the *Efficient Frontier*, its optimised portfolios for *Minimum Volatility* and *Maximum Sharpe Ratio*, as well as the portfolio's individual stocks. 72 | 73 | .. note:: This example refers to ``example/Example-Optimisation.py`` of the `GitHub`_ repository. It can be downloaded with jupyter notebook cell information: :download:`download Example-Optimisation.py <../example/Example-Optimisation.py>` 74 | 75 | .. literalinclude:: ./auto-Example-Optimisation.py 76 | :linenos: 77 | :language: python 78 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | .. figure:: ../images/finquant-logo.png 4 | :scale: 40 % 5 | :align: center 6 | 7 | .. |fin| image:: ../images/finquant-logo-bw.png 8 | :width: 30 9 | :align: middle 10 | 11 | .. _GitHub Actions: https://github.com/fmilthaler/finquant/actions/workflows/pytest.yml/badge.svg?branch=master 12 | 13 | .. _GitHub: https://github.com/fmilthaler/FinQuant/ 14 | 15 | .. _PyPI: https://pypi.org/project/FinQuant/ 16 | 17 | ################################### 18 | Welcome to FinQuant's documentation 19 | ################################### 20 | 21 | *FinQuant* is a program for financial portfolio management, analysis and optimisation. It is designed to generate an object that holds your data, e.g. stock prices of different stocks, which automatically computes the most common quantities, such as *Expected annual Return*, *Volatility* and *Sharpe Ratio*. Moreover, it provides a library for computing different kinds of *Returns* and visualising *Moving Averages* and *Bollinger Bands*. Finally, given a set of stocks, it also allows for finding optimised portfolios. 22 | 23 | *FinQuant* is made to be easily extended. I hope it proves itself useful for hobby investors, students, geeks, and the intellectual curious. 24 | 25 | .. caution:: While *FinQuant* has tests in place that are run automatically by `GitHub Actions`_, it cannot guarantee to be bug free, nor that the analysis or optimised portfolio yield to wealth. Please use at your own discretion and refer to the :ref:`license`. 26 | 27 | 28 | Installation 29 | ============ 30 | As it is common for open-source projects, there are several ways to get hold of the code. Choose whichever suits you and your purposes best. 31 | 32 | Dependencies 33 | ------------ 34 | 35 | *FinQuant* depends on the following Python packages: 36 | 37 | - ``python>=3.10.0`` 38 | - ``numpy>=1.15`` 39 | - ``scipy>=1.2.0`` 40 | - ``pandas>=2.0`` 41 | - ``matplotlib>=3.0`` 42 | - ``quandl>=3.4.5`` 43 | - ``yfinance>=0.1.43`` 44 | 45 | From PyPI 46 | --------- 47 | *FinQuant* can be obtained from `PyPI`_: 48 | 49 | .. code:: text 50 | 51 | pip install FinQuant 52 | 53 | From GitHub 54 | ----------- 55 | Get the code from `GitHub`_: 56 | 57 | .. code:: text 58 | 59 | git clone https://github.com/fmilthaler/FinQuant.git 60 | 61 | Then inside ``FinQuant`` run: 62 | 63 | .. code:: text 64 | 65 | python setup.py install 66 | 67 | Alternatively, if you do not wish to install *FinQuant*, you can also download/clone it as stated above, and then make sure to add it to your ``PYTHONPATH``. 68 | 69 | 70 | ################# 71 | Table of Contents 72 | ################# 73 | .. toctree:: 74 | :maxdepth: 2 75 | 76 | quickstart 77 | examples 78 | portfolio 79 | assets 80 | quants 81 | returns 82 | movingaverage 83 | efficientfrontier 84 | montecarlo 85 | developers 86 | license 87 | about 88 | 89 | 90 | ################## 91 | Indices and tables 92 | ################## 93 | 94 | * :ref:`genindex` 95 | * :ref:`modindex` 96 | * :ref:`search` 97 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | ####### 4 | License 5 | ####### 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/montecarlo.rst: -------------------------------------------------------------------------------- 1 | .. _montecarlo: 2 | 3 | ########### 4 | Monte Carlo 5 | ########### 6 | 7 | The *Monte Carlo* method is implemented in ``finquant.monte_carlo``. 8 | 9 | 10 | .. automodule:: finquant.monte_carlo 11 | 12 | .. autoclass:: finquant.monte_carlo.MonteCarlo 13 | :members: 14 | 15 | .. automethod:: __init__ 16 | 17 | .. autoclass:: finquant.monte_carlo.MonteCarloOpt 18 | :members: 19 | 20 | .. automethod:: __init__ 21 | -------------------------------------------------------------------------------- /docs/movingaverage.rst: -------------------------------------------------------------------------------- 1 | .. _movingaverage: 2 | 3 | ############## 4 | Moving Average 5 | ############## 6 | 7 | *Moving Averages* are implemented in ``finquant.moving_average``. 8 | 9 | .. automodule:: finquant.moving_average 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/portfolio.rst: -------------------------------------------------------------------------------- 1 | .. _portfolio: 2 | 3 | #################### 4 | Portfolio Management 5 | #################### 6 | 7 | As mentioned above, *FinQuant* is a program for financial portfolio management, among others. 8 | The module ``finquant.portfolio`` does exactly that. 9 | 10 | .. note:: The impatient reader who simply wants to jump in and start using *FinQuant* is advised 11 | to jump to `build_portfolio`_ and have a look at and play around with the :ref:`examples`. 12 | 13 | .. automodule:: finquant.portfolio 14 | 15 | 16 | Portfolio 17 | ========= 18 | .. autoclass:: finquant.portfolio.Portfolio 19 | :members: 20 | 21 | .. automethod:: __init__ 22 | 23 | 24 | build_portfolio 25 | =============== 26 | .. automethod:: finquant.portfolio.build_portfolio 27 | -------------------------------------------------------------------------------- /docs/quants.rst: -------------------------------------------------------------------------------- 1 | .. _quants: 2 | 3 | ######################################### 4 | Expected Return, Volatility, Sharpe Ratio 5 | ######################################### 6 | 7 | The *Expected Return*, *Volatility* and *Sharpe Ratio* of a portfolio are computed with the module ``finquant.quants``. 8 | 9 | .. automodule:: finquant.quants 10 | :members: 11 | 12 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | .. _quandl: http://www.quandl.com/ 3 | .. _yfinance: https://pypi.org/project/yfinance/ 4 | .. |yahoofinance| replace:: Yahoo Finance 5 | .. _yahoofinance: https://finance.yahoo.com/ 6 | 7 | ########### 8 | Quick Start 9 | ########### 10 | 11 | This section covers some quick examples of *FinQuant*'s features. For a full overview please continue with the documentation, and/or have a look at :ref:`examples`. 12 | 13 | Building a Portfolio 14 | ==================== 15 | 16 | Getting an object of ``Portfolio`` that holds stock prices of four different stocks, as well as its properties and interfaces to optimisation methods is as simple as: 17 | 18 | .. code-block:: python 19 | 20 | from finquant.portfolio import build_portfolio 21 | names = ['GOOG', 'AMZN', 'MCD', 'DIS'] 22 | pf = build_portfolio(names=names) 23 | 24 | The above uses *Quandl* in the background to download the requested data. For more information on *Quandl*, please refer to quandl_. 25 | 26 | If preferred, *FinQuant* also allows to fetch stock price data from |yahoofinance|_. The code snippet below is the equivalent to the above, but using yfinance_ instead (default value for ``data_api`` is ``"quandl"``): 27 | 28 | .. code-block:: python 29 | 30 | from finquant.portfolio import build_portfolio 31 | names = ['GOOG', 'AMZN', 'MCD', 'DIS'] 32 | pf = build_portfolio(names=names, data_api="yfinance") 33 | 34 | Alternatively, if you already are in possession of stock prices you want to analyse/optimise, you can do the following. 35 | 36 | .. code-block:: python 37 | 38 | import pathlib 39 | from finquant.portfolio import build_portfolio 40 | df_data_path = pathlib.Path() / 'data' / 'ex1-stockdata.csv' 41 | df_data = pd.read_csv(df_data_path, index_col='Date', parse_dates=True) 42 | # building a portfolio by providing stock data 43 | pf = build_portfolio(data=df_data) 44 | 45 | For this to work, the data is required to be a ``pandas.DataFrame`` with stock prices as columns. 46 | 47 | 48 | Properties of the Portfolio 49 | =========================== 50 | 51 | The portfolio's properties are automatically computed as it is being built. One can have a look at them with 52 | 53 | .. code-block:: python 54 | 55 | pf.properties() 56 | 57 | which shows 58 | 59 | .. code-block:: python 60 | 61 | ---------------------------------------------------------------------- 62 | Stocks: GOOG, AMZN, MCD, DIS 63 | Time window/frequency: 252 64 | Risk free rate: 0.005 65 | Portfolio Expected Return: 0.266 66 | Portfolio Volatility: 0.156 67 | Portfolio Sharpe Ratio: 1.674 68 | 69 | Skewness: 70 | GOOG AMZN MCD DIS 71 | 0 0.124184 0.087516 0.58698 0.040569 72 | 73 | Kurtosis: 74 | GOOG AMZN MCD DIS 75 | 0 -0.751818 -0.856101 -0.602008 -0.892666 76 | 77 | Information: 78 | Allocation Name 79 | 0 0.25 GOOG 80 | 1 0.25 AMZN 81 | 2 0.25 MCD 82 | 3 0.25 DIS 83 | ---------------------------------------------------------------------- 84 | 85 | Moving Averages 86 | =============== 87 | 88 | *Moving Averages* and *Bollinger Bands* can be computed and visualised with the help of the module ``finquant.moving_average``. 89 | 90 | .. note:: When computing/visualising a *band* of Moving Averages, ``compute_ma`` automatically finds the buy/sell signals based on the minimum/maximum *Moving Average* that were computed and highlights those with arrow up/down markers. 91 | 92 | .. code-block:: python 93 | 94 | from finquant.moving_average import compute_ma, ema 95 | # get stock data for Disney 96 | dis = pf.get_stock("DIS").data.copy(deep=True) 97 | spans = [10, 50, 100, 150, 200] 98 | # computing and visualising a band of moving averages 99 | ma = compute_ma(dis, ema, spans, plot=True) 100 | print(ma.tail()) 101 | 102 | which results in 103 | 104 | .. code:: 105 | 106 | DIS 10d 50d 100d 150d 200d 107 | Date 108 | 2017-12-22 108.67 109.093968 104.810423 103.771618 103.716741 103.640858 109 | 2017-12-26 108.12 108.916883 104.940210 103.857724 103.775063 103.685426 110 | 2017-12-27 107.64 108.684722 105.046085 103.932621 103.826254 103.724775 111 | 2017-12-28 107.77 108.518409 105.152905 104.008608 103.878489 103.765026 112 | 2017-12-29 107.51 108.335062 105.245340 104.077943 103.926588 103.802290 113 | 114 | .. figure:: ../images/ma-band-buysell-signals.svg 115 | :width: 85 % 116 | :align: center 117 | 118 | 119 | Portfolio Optimisation 120 | ====================== 121 | *FinQuant* allows the optimisation of financial portfolios along the *Efficient Frontier* by minimising a cost/objective function. *FinQuant* uses the Python package ``scipy`` for the minimisation. Alternatively, a *Monte Carlo* approach is implemented as well. The below demonstrates how *FinQuant* performs such an optimisation and visualisation of the results. 122 | 123 | .. code-block:: python 124 | 125 | # Monte Carlo optimisation 126 | opt_w, opt_res = pf.mc_optimisation(num_trials=5000) 127 | pf.mc_plot_results() 128 | # minimisation to compute efficient frontier and optimal portfolios along it 129 | pf.ef_plot_efrontier() 130 | pf.ef.plot_optimal_portfolios() 131 | # plotting individual stocks 132 | pf.plot_stocks() 133 | 134 | .. figure:: ../images/ef-mc-overlay.svg 135 | :width: 85 % 136 | :align: center 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/returns.rst: -------------------------------------------------------------------------------- 1 | .. _returns: 2 | 3 | ############################ 4 | Daily and Historical Returns 5 | ############################ 6 | 7 | Returns are implemented in ``finquant.returns``. 8 | 9 | .. automodule:: finquant.returns 10 | :members: 11 | -------------------------------------------------------------------------------- /example/Example-Analysis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 4 3 | 4 | # 5 | 6 | # # Example: 7 | # ## Building a portfolio with `build_portfolio()` with data obtained from data files. 8 | # Note: The stock data is provided in two data files. The stock data was previously pulled from quandl. 9 | 10 | # 11 | 12 | import datetime 13 | import pathlib 14 | 15 | import matplotlib.pyplot as plt 16 | import pandas as pd 17 | 18 | # importing FinQuant's function to automatically build the portfolio 19 | from finquant.portfolio import build_portfolio 20 | 21 | # 22 | 23 | # plotting style: 24 | plt.style.use("seaborn-v0_8-darkgrid") 25 | # set line width 26 | plt.rcParams["lines.linewidth"] = 2 27 | # set font size for titles 28 | plt.rcParams["axes.titlesize"] = 14 29 | # set font size for labels on axes 30 | plt.rcParams["axes.labelsize"] = 12 31 | # set size of numbers on x-axis 32 | plt.rcParams["xtick.labelsize"] = 10 33 | # set size of numbers on y-axis 34 | plt.rcParams["ytick.labelsize"] = 10 35 | # set figure size 36 | plt.rcParams["figure.figsize"] = (10, 6) 37 | 38 | # 39 | 40 | # ## Building a portfolio with `build_portfolio()` 41 | # As in previous example, using `build_portfolio()` to generate an object of `Portfolio`. 42 | 43 | # 44 | 45 | # read data from files: 46 | df_data_path = pathlib.Path.cwd() / ".." / "data" / "ex1-stockdata.csv" 47 | df_data = pd.read_csv(df_data_path, index_col="Date", parse_dates=True) 48 | # building a portfolio by providing stock data 49 | pf = build_portfolio(data=df_data) 50 | 51 | # 52 | 53 | # ## Expected Return, Volatility, Sharpe Ratio, Sortino Ratio, and Value at Risk of Portfolio 54 | # The annualised expected return and volatility, as well as the Sharpe Ratio, the Sortino Ratio, and Value at Risk are automatically computed. 55 | # They are obtained as shown below. 56 | # The expected return and volatility are based on 252 trading days by default. 57 | # The Sharpe Ratio and the Sortino ratio are computed with a risk free rate of 0.005 by default. 58 | # The Value at Risk is computed with a confidence level of 0.95 by default. 59 | 60 | # 61 | 62 | # expected (annualised) return 63 | print(pf.expected_return) 64 | 65 | # 66 | 67 | # volatility 68 | print(pf.volatility) 69 | 70 | # 71 | 72 | # Sharpe Ratio (computed with a risk free rate of 0.005 by default) 73 | print(pf.sharpe) 74 | 75 | # 76 | 77 | # Sortino Ratio (computed with a risk free rate of 0.005 by default) 78 | print(pf.sortino) 79 | 80 | # 81 | 82 | # Value at Risk (computed with a confidence level of 0.95 by default) 83 | print(pf.var) 84 | 85 | # 86 | 87 | # ## Getting Skewness and Kurtosis of the stocks 88 | 89 | # 90 | 91 | print(pf.skew) 92 | 93 | # 94 | 95 | print(pf.kurtosis) 96 | 97 | # 98 | 99 | # ## Nicely printing out portfolio quantities 100 | # To print the expected annualised return, volatility, Sharpe Ratio, Sortino Ratio, skewness and Kurtosis of the portfolio and its stocks, one can simply do `pf.properties()`. 101 | 102 | # 103 | 104 | print(pf) 105 | pf.properties() 106 | 107 | # 108 | 109 | # ## Daily returns and log returns 110 | # `FinQuant` provides functions to compute daily returns and annualised mean returns of a given DataFrame in various ways. 111 | 112 | # 113 | 114 | # annualised mean returns 115 | print(pf.comp_mean_returns()) 116 | 117 | # 118 | 119 | # daily returns (percentage change) 120 | print(pf.comp_cumulative_returns().head(3)) 121 | 122 | # 123 | 124 | print(pf.comp_daily_log_returns().head(3)) 125 | 126 | # 127 | 128 | # plotting stock data of portfolio 129 | pf.data.plot() 130 | plt.show() 131 | 132 | # 133 | 134 | # The stock prices of Google and Amazon are much higher than those for McDonald's and Disney. Hence the fluctuations of the latter ones are barely seen in the above plot. One can use `pandas.plot()` method to create a secondary y axis. 135 | 136 | # 137 | 138 | pf.data.plot(secondary_y=["WIKI/MCD", "WIKI/DIS"], grid=True) 139 | plt.show() 140 | 141 | # 142 | 143 | # plotting cumulative returns (price_{t} - price_{t=0}) / price_{t=0} 144 | pf.comp_cumulative_returns().plot().axhline(y=0, color="black", lw=3) 145 | plt.show() 146 | 147 | # 148 | 149 | # plotting daily percentage changes of returns 150 | pf.comp_daily_returns().plot().axhline(y=0, color="black") 151 | plt.show() 152 | 153 | # 154 | 155 | # plotting daily log returns 156 | pf.comp_daily_log_returns().plot().axhline(y=0, color="black") 157 | plt.show() 158 | 159 | # 160 | 161 | # cumulative log returns 162 | pf.comp_daily_log_returns().cumsum().plot().axhline(y=0, color="black") 163 | plt.show() 164 | 165 | # 166 | 167 | # ## Moving Averages 168 | # `FinQuant` provides a module `finquant.moving_average` to compute moving averages. See below. 169 | 170 | # 171 | 172 | from finquant.moving_average import sma 173 | 174 | # simple moving average 175 | ax = pf.data.plot(secondary_y=["WIKI/MCD", "WIKI/DIS"], grid=True) 176 | # computing simple moving average over a span of 50 (trading) days 177 | # and plotting it 178 | sma(pf.data, span=50).plot(ax=ax, secondary_y=["WIKI/MCD", "WIKI/DIS"], grid=True) 179 | plt.show() 180 | 181 | # 182 | 183 | from finquant.moving_average import ema 184 | 185 | # exponential moving average 186 | ax = pf.data.plot(secondary_y=["WIKI/MCD", "WIKI/DIS"], grid=True) 187 | # computing exponential moving average and plotting it 188 | ema(pf.data).plot(ax=ax, secondary_y=["WIKI/MCD", "WIKI/DIS"]) 189 | plt.show() 190 | 191 | # 192 | 193 | # ## Band of moving averages and Buy/Sell signals 194 | # `FinQuant` also provides a method `finquant.moving_average.compute_ma` that automatically computes and plots several moving averages. It also **finds buy/sell signals based on crossovers** of the shortest and longest moving average. 195 | # To learn more about it and its input arguments, read its docstring and see the example below. 196 | 197 | # 198 | 199 | from finquant.moving_average import compute_ma 200 | 201 | print(compute_ma.__doc__) 202 | 203 | # 204 | 205 | # get stock data for disney 206 | dis = pf.get_stock("WIKI/DIS").data.copy(deep=True) 207 | # we want moving averages of 10, 50, 100, and 200 days. 208 | spans = [10, 50, 100, 150, 200] 209 | # compute and plot moving averages 210 | dis_ma = compute_ma(dis, ema, spans, plot=True) 211 | plt.show() 212 | 213 | # 214 | 215 | # ## Plot the Bollinger Band of one stock 216 | # The Bollinger Band can be automatically computed and plotted with the method `finquant.moving_average.plot_bollinger_band`. See below for an example. 217 | 218 | # 219 | 220 | # plot the bollinger band of the disney stock prices 221 | from finquant.moving_average import plot_bollinger_band 222 | 223 | # get stock data for disney 224 | dis = pf.get_stock("WIKI/DIS").data.copy(deep=True) 225 | span = 20 226 | # for simple moving average: 227 | plot_bollinger_band(dis, sma, span) 228 | plt.show() 229 | # for exponential moving average: 230 | plot_bollinger_band(dis, ema, span) 231 | plt.show() 232 | 233 | # 234 | 235 | # ## Recomputing expected return, volatility and Sharpe ratio 236 | # **Note**: When doing so, the instance variables for 237 | # - Expected return 238 | # - Volatility 239 | # - Sharpe Ratio 240 | # are automatically recomputed. 241 | 242 | # 243 | 244 | # If the return, volatility and Sharpe ratio need to be computed based 245 | # on a different time window and/or risk free rate, one can recompute 246 | # those values as shown below 247 | # 1. set the new value(s) 248 | pf.freq = 100 249 | pf.risk_free_rate = 0.02 250 | 251 | # 2.a compute and get new values based on new freq/risk_free_rate 252 | exret = pf.comp_expected_return(freq=100) 253 | vol = pf.comp_volatility(freq=100) 254 | sharpe = pf.comp_sharpe() 255 | print( 256 | "For {} trading days and a risk free rate of {}:".format(pf.freq, pf.risk_free_rate) 257 | ) 258 | print("Expected return: {:0.3f}".format(exret)) 259 | print("Volatility: {:0.3f}".format(vol)) 260 | print("Sharpe Ratio: {:0.3f}".format(sharpe)) 261 | 262 | # 2.b print out properties of portfolio (which is based on new freq/risk_free_rate) 263 | pf.properties() 264 | 265 | # 266 | 267 | # ## Extracting data of stocks individually 268 | # Each stock (its information and data) of the portfolio is stored as a `Stock` data structure. If needed, one can of course extract the relevant data from the portfolio DataFrame, or access the `Stock` instance. The commands are very similar to the once for `Portfolio`. See below how it can be used. 269 | 270 | # 271 | 272 | # getting Stock object from portfolio, for Google's stock 273 | goog = pf.get_stock("WIKI/GOOG") 274 | # getting the stock prices 275 | goog_prices = goog.data 276 | print(goog_prices.head(3)) 277 | 278 | # 279 | 280 | print(goog.comp_daily_returns().head(3)) 281 | 282 | # 283 | 284 | print(goog.expected_return) 285 | 286 | # 287 | 288 | print(goog.volatility) 289 | 290 | # 291 | 292 | print(goog.skew) 293 | 294 | # 295 | 296 | print(goog.kurtosis) 297 | 298 | # 299 | 300 | print(goog) 301 | goog.properties() 302 | 303 | # 304 | 305 | # ## Extracting stock data by date 306 | # Since quandl provides a DataFrame with an index of dates, it is easy to extract data from the portfolio for a given time frame. Three examples are shown below. 307 | 308 | # 309 | 310 | print(pf.data.loc[str(datetime.datetime(2015, 1, 2))]) 311 | 312 | # 313 | 314 | print(pf.data.loc[pf.data.index > datetime.datetime(2016, 1, 2)].head(3)) 315 | 316 | # 317 | 318 | print(pf.data.loc[pf.data.index.year == 2017].head(3)) 319 | -------------------------------------------------------------------------------- /example/Example-Build-Portfolio-from-file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 4 3 | 4 | # 5 | 6 | # # Building a portfolio with data from disk 7 | # ## Building a portfolio with `build_portfolio()` with data obtained from data files. 8 | # Note: The stock data is provided in two data files. The stock data was previously pulled from quandl. 9 | 10 | # 11 | 12 | import datetime 13 | import pathlib 14 | 15 | import pandas as pd 16 | 17 | # importing FinQuant's function to automatically build the portfolio 18 | from finquant.portfolio import build_portfolio 19 | 20 | # 21 | 22 | # ### Get data from disk/file 23 | # Here we use `pandas.read_cvs()` method to read in the data. 24 | 25 | # 26 | 27 | # stock data was previously pulled from quandl and stored in ex1-stockdata.csv 28 | # commands used to save data: 29 | # pf.portfolio.to_csv("ex1-portfolio.csv", encoding='utf-8', index=False, header=True) 30 | # pf.data.to_csv("ex1-stockdata.csv", encoding='utf-8', index=True, index_label="Date") 31 | # read data from files: 32 | df_pf_path = pathlib.Path.cwd() / ".." / "data" / "ex1-portfolio.csv" 33 | df_data_path = pathlib.Path.cwd() / ".." / "data" / "ex1-stockdata.csv" 34 | df_pf = pd.read_csv(df_pf_path) 35 | df_data = pd.read_csv(df_data_path, index_col="Date", parse_dates=True) 36 | 37 | # 38 | 39 | # ### Examining the DataFrames 40 | 41 | # 42 | 43 | print(df_pf) 44 | 45 | # 46 | 47 | print(df_data.head(3)) 48 | 49 | # 50 | 51 | # ## Building a portfolio with `build_portfolio()` 52 | # `build_portfolio()` is an interface that can be used in different ways. Two of which is shown below. For more information the docstring is shown below as well. 53 | # In this example `build_portfolio()` is being passed `df_data`, which was read in from file above. 54 | 55 | # 56 | 57 | print(build_portfolio.__doc__) 58 | 59 | # 60 | 61 | # ## Building a portfolio with data only 62 | # Below is an example of only passing a `DataFrame` containing data (e.g. stock prices) to `build_portfolio()` in order to build an instance of `Portfolio`. In this case, the allocation of stocks is automatically generated by equally distributing the weights across all stocks. 63 | 64 | # 65 | 66 | # building a portfolio by providing stock data 67 | pf = build_portfolio(data=df_data) 68 | 69 | # 70 | 71 | # ### Portfolio is successfully built 72 | # Below it is shown how the allocation of the stocks and the data (e.g. prices) of the stocks can be obtained from the object `pf`. 73 | 74 | # 75 | 76 | # the portfolio information DataFrame 77 | print(pf.portfolio.name) 78 | print(pf.portfolio) 79 | 80 | # 81 | 82 | # the portfolio stock data, prices DataFrame 83 | print(pf.data.head(3)) 84 | 85 | # 86 | 87 | # ## Building a portfolio with data and desired allocation 88 | # If a specific allocation of stocks in the portfolio is desired, a `DataFrame` such as `df_pf` (which was read from file above) can be passed to `build_portfolio()` as shown below. 89 | 90 | # 91 | 92 | # building a portfolio by providing stock data 93 | # and a desired allocation 94 | pf2 = build_portfolio(data=df_data, pf_allocation=df_pf) 95 | 96 | # 97 | 98 | # the portfolio information DataFrame 99 | print(pf2.portfolio.name) 100 | print(pf2.portfolio) 101 | 102 | # 103 | 104 | # the portfolio stock data, prices DataFrame 105 | print(pf2.data.head(3)) 106 | -------------------------------------------------------------------------------- /example/Example-Build-Portfolio-from-web.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 4 3 | 4 | # 5 | 6 | # # Building a portfolio with data from `quandl`/`yfinance` 7 | # ## Building a portfolio with `build_portfolio()` by downloading relevant data through `quandl`/`yfinance` with stock names, start and end date and column labels 8 | # This example only focuses on how to use `build_portfolio()` to get an instance of `Portfolio` by providing a few items of information that is passed on to `quandl`/`yfinance`. For a more exhaustive description of this package and example, please try `Example-Analysis` and `Example-Optimisation`. 9 | 10 | # 11 | 12 | import datetime 13 | 14 | import pandas as pd 15 | 16 | # importing some custom functions/objects 17 | from finquant.portfolio import build_portfolio 18 | 19 | # 20 | 21 | # ## Get data from `quandl`/`yfinance` and build portfolio 22 | # First we need to build a pandas.DataFrame that holds relevant data for our portfolio. The minimal information needed are stock names and the amount of money to be invested in them, e.g. Allocation. 23 | 24 | # 25 | 26 | # To play around yourself with different stocks, here is a short list of companies and their tickers on Yahoo Finance: 27 | # d = {0: {'Name':'GOOG', 'Allocation':20}, # Google 28 | # 1: {'Name':'AMZN', 'Allocation':33}, # Amazon 29 | # 2: {'Name':'MSFT', 'Allocation':18}, # Microsoft 30 | # 3: {'Name':'AAPL', 'Allocation':10}, # Apple 31 | # 4: {'Name':'KO', 'Allocation':15}, # Coca-Cola 32 | # 5: {'Name':'XOM', 'Allocation':11}, # Exxon Mobil 33 | # 6: {'Name':'JPM', 'Allocation':21}, # JP Morgan 34 | # 7: {'Name':'DIS', 'Allocation':9}, # Disney 35 | # 8: {'Name':'MCD', 'Allocation':23}, # McDonald's 36 | # 9: {'Name':'WMT', 'Allocation':3}, # Walmart 37 | # 10: {'Name':'GS', 'Allocation':9}, # Goldman Sachs 38 | # } 39 | 40 | # 41 | 42 | d = { 43 | 0: {"Name": "GOOG", "Allocation": 20}, 44 | 1: {"Name": "AMZN", "Allocation": 10}, 45 | 2: {"Name": "MCD", "Allocation": 15}, 46 | 3: {"Name": "DIS", "Allocation": 18}, 47 | } 48 | # If you wish to use `quandl` as source, you must add "WIKI/" at the beginning of stock names/tickers, as "WIKI/GOOG". 49 | 50 | pf_allocation = pd.DataFrame.from_dict(d, orient="index") 51 | 52 | # 53 | 54 | # ### User friendly interface to quandl/yfinance 55 | # As mentioned above, in this example `build_portfolio()` is used to build a portfolio by performing a query to `quandl`/`yfinance`. We mention that `quandl` will be removed in future versions of `FinQuant` as it is deprecated. 56 | # 57 | # To download Google's stock data, `quandl` requires the string `"WIKI/GOOG"` and `yfinance` the string `"GOOG"`. 58 | # For simplicity, `FinQuant` facilitates a set of functions under the hood to sort out lots of specific commands/required input for `quandl`/`yfinance`. When using `FinQuant`, the user simply needs to provide a list of stock names/tickers. 59 | # For example, if using `quandl` as a data source (currently the default option), a list of names/tickers as shown below is a valid input for `FinQuant`'s function `build_portfolio(names=names)`: 60 | # * `names = ["WIKI/GOOG", "WIKI/AMZN"]` 61 | # 62 | # If using `yfinance` as a data source, `FinQuant`'s function `build_portfolio(names=names)` expects the stock names to be without any leading/trailing string (check Yahoo Finance for correct stock names): 63 | # * `names = ["GOOG", "AMZN"]` 64 | # 65 | # By default, `FinQuant` currently uses `quandl` to obtain stock price data. The function `build_portfolio()` can be called with the optional argument `data_api` to use `yfinance` instead: 66 | # * `build_portfolio(names=names, data_api="yfinance")` 67 | # 68 | # In the below example we are using `yfinance` to download stock data. We specify the start and end date of the stock prices to be downloaded. 69 | # We also provide the optional parameter `market_index` to download the historical data of a market index. 70 | # `FinQuant` can use them to calculate the Treynor Ratio, beta parameter, and R squared coefficient, measuring the portfolio's daily volatility compared to the market. 71 | 72 | # 73 | 74 | # here we set the list of names based on the names in 75 | # the DataFrame pf_allocation 76 | names = pf_allocation["Name"].values.tolist() 77 | 78 | # dates can be set as datetime or string, as shown below: 79 | start_date = datetime.datetime(2015, 1, 1) 80 | end_date = "2017-12-31" 81 | 82 | # the market index used to compare the portfolio to (in this case S&P 500). 83 | # If the parameter is omitted, no market comparison will be done 84 | market_index = "^GSPC" 85 | 86 | # While quandl/yfinance will download lots of different prices for each stock, 87 | # e.g. high, low, close, etc, FinQuant will extract the column "Adj. Close" ("Adj Close" if using yfinance). 88 | 89 | pf = build_portfolio( 90 | names=names, 91 | pf_allocation=pf_allocation, 92 | start_date=start_date, 93 | end_date=end_date, 94 | data_api="yfinance", 95 | market_index=market_index, 96 | ) 97 | 98 | # 99 | 100 | # ## Portfolio is successfully built 101 | # Getting data from the portfolio 102 | 103 | # 104 | 105 | # the portfolio information DataFrame 106 | print(pf.portfolio) 107 | 108 | # 109 | 110 | # the portfolio stock data, prices DataFrame 111 | print(pf.data.head(3)) 112 | 113 | # 114 | 115 | # print out information and quantities of given portfolio 116 | print(pf) 117 | pf.properties() 118 | 119 | # 120 | 121 | # ## Please continue with `Example-Build-Portfolio-from-file.py`. 122 | # As mentioned above, this example only shows how to use `build_portfolio()` to get an instance of `Portfolio` by downloading data through `quandl`/`yfinance`. 123 | -------------------------------------------------------------------------------- /example/Example-Optimisation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 4 3 | 4 | # 5 | 6 | # # Example: Portfolio optimisation 7 | # This example shows how `FinQuant` can be used to optimise a portfolio. 8 | # Two different approaches are implemented in `FinQuant`: 9 | # 1. Efficient Frontier 10 | # 2. Monte Carlo run 11 | # With the *Efficient Frontier* approach, the portfolio can be optimised for 12 | # - minimum volatility, 13 | # - maximum Sharpe ratio 14 | # - minimum volatility for a given expected return 15 | # - maximum Sharpe ratio for a given target volatility 16 | # by performing a numerical solve to minimise/maximise an objective function. 17 | # Alternatively a *Monte Carlo* run of `n` trials can be performed to find the optimal portfolios for 18 | # - minimum volatility, 19 | # - maximum Sharpe ratio 20 | # The approach branded as *Efficient Frontier* should be the preferred method for reasons of computational effort and accuracy. The latter approach is only included for the sake of completeness, and creation of beautiful plots. 21 | # 22 | # ## Visualisation 23 | # Not only does `FinQuant` allow for the optimisation of a portfolio with the above mentioned methods and objectives, `FinQuant` also allows for the computation and visualisation of an *Efficient Frontier* and *Monte Carlo* run. 24 | # Let `pf` be an instance of `Portfolio`. The *Efficient Frontier* can be computed and visualised with `pf.ef_plot_efrontier()`. The optimal portfolios for *minimum volatility* and *maximum Sharpe ratio* can be visualised with `pf.ef_plot_optimal_portfolios()`. And if required, the individual stocks of the portfolio can be visualised with `pf.plot_stocks(show=False)`. An overlay of these three commands is shown below. 25 | # Finally, the entire result of a *Monte Carlo* run can also be visualised automatically by `FinQuant`. An example is shown below. 26 | 27 | # 28 | 29 | import datetime 30 | import pathlib 31 | 32 | import matplotlib.pyplot as plt 33 | import numpy as np 34 | import pandas as pd 35 | 36 | # importing FinQuant's function to automatically build the portfolio 37 | from finquant.portfolio import build_portfolio 38 | 39 | # 40 | 41 | # plotting style: 42 | plt.style.use("seaborn-v0_8-darkgrid") 43 | # set line width 44 | plt.rcParams["lines.linewidth"] = 2 45 | # set font size for titles 46 | plt.rcParams["axes.titlesize"] = 14 47 | # set font size for labels on axes 48 | plt.rcParams["axes.labelsize"] = 12 49 | # set size of numbers on x-axis 50 | plt.rcParams["xtick.labelsize"] = 10 51 | # set size of numbers on y-axis 52 | plt.rcParams["ytick.labelsize"] = 10 53 | # set figure size 54 | plt.rcParams["figure.figsize"] = (10, 6) 55 | 56 | # 57 | 58 | # ### Get data from disk/file 59 | # Here we use `pandas.read_cvs()` method to read in the data. 60 | 61 | # 62 | 63 | # stock data was previously pulled from quandl and stored in ex1-stockdata.csv 64 | # read data from files: 65 | df_data_path = pathlib.Path.cwd() / ".." / "data" / "ex1-stockdata.csv" 66 | df_data = pd.read_csv(df_data_path, index_col="Date", parse_dates=True) 67 | # building a portfolio by providing stock data 68 | pf = build_portfolio(data=df_data) 69 | print(pf) 70 | pf.properties() 71 | 72 | # 73 | 74 | # # Portfolio optimisation 75 | # ## Efficient Frontier 76 | # Based on the **Efficient Frontier**, the portfolio can be optimised for 77 | # - minimum volatility 78 | # - maximum Sharpe ratio 79 | # - minimum volatility for a given target return 80 | # - maximum Sharpe ratio for a given target volatility 81 | # See below for an example for each optimisation. 82 | 83 | # 84 | 85 | # if needed, change risk free rate and frequency/time window of the portfolio 86 | print("pf.risk_free_rate = {}".format(pf.risk_free_rate)) 87 | print("pf.freq = {}".format(pf.freq)) 88 | 89 | # 90 | 91 | pf.ef_minimum_volatility(verbose=True) 92 | 93 | # 94 | 95 | # optimisation for maximum Sharpe ratio 96 | pf.ef_maximum_sharpe_ratio(verbose=True) 97 | 98 | # 99 | 100 | # minimum volatility for a given target return of 0.26 101 | pf.ef_efficient_return(0.26, verbose=True) 102 | 103 | # 104 | 105 | # maximum Sharpe ratio for a given target volatility of 0.22 106 | pf.ef_efficient_volatility(0.22, verbose=True) 107 | 108 | # 109 | 110 | # ### Manually creating an instance of EfficientFrontier 111 | # If required, or preferred, the below code shows how the same is achieved by manually creating an instance of EfficientFrontier, passing it the mean returns and covariance matrix of the previously assembled portfolio. 112 | 113 | # 114 | 115 | from finquant.efficient_frontier import EfficientFrontier 116 | 117 | # creating an instance of EfficientFrontier 118 | ef = EfficientFrontier(pf.comp_mean_returns(freq=1), pf.comp_cov()) 119 | # optimisation for minimum volatility 120 | print(ef.minimum_volatility()) 121 | 122 | # 123 | 124 | # printing out relevant quantities of the optimised portfolio 125 | (expected_return, volatility, sharpe) = ef.properties(verbose=True) 126 | 127 | # 128 | 129 | # ### Computing and visualising the Efficient Frontier 130 | # `FinQuant` offers several ways to compute the *Efficient Frontier*. 131 | # 1. Through the opject `pf` 132 | # - with automatically setting limits of the *Efficient Frontier* 133 | # 2. By manually creating an instance of `EfficientFrontier` and providing the data from the portfolio 134 | # - with automatically setting limits of the *Efficient Frontier* 135 | # - by providing a range of target expected return values 136 | # As before, let `pf` and be an instance of `Portfolio`. The following code snippets compute and plot an *Efficient Frontier* of a portfolio, its optimised portfolios and individual stocks within the portfolio. 137 | # - `pf.ef_plot_efrontier()` 138 | # - `pf.ef_plot_optimal_portfolios()` 139 | # - `pf.plot_stocks()` 140 | 141 | # 142 | 143 | # #### Efficient Frontier of `pf` 144 | 145 | # 146 | 147 | # computing and plotting efficient frontier of pf 148 | pf.ef_plot_efrontier() 149 | # adding markers to optimal solutions 150 | pf.ef_plot_optimal_portfolios() 151 | # and adding the individual stocks to the plot 152 | pf.plot_stocks() 153 | plt.show() 154 | 155 | # 156 | 157 | # #### Manually creating an Efficient Frontier with target return values 158 | 159 | # 160 | 161 | targets = np.linspace(0.12, 0.45, 50) 162 | # computing efficient frontier 163 | efficient_frontier = ef.efficient_frontier(targets) 164 | # plotting efficient frontier 165 | ef.plot_efrontier() 166 | # adding markers to optimal solutions 167 | pf.ef.plot_optimal_portfolios() 168 | # and adding the individual stocks to the plot 169 | pf.plot_stocks() 170 | plt.show() 171 | 172 | # 173 | 174 | # ## Monte Carlo 175 | # Perform a Monte Carlo run to find the portfolio with the minimum volatility and maximum Sharpe Ratio. 176 | 177 | # 178 | 179 | opt_w, opt_res = pf.mc_optimisation(num_trials=5000) 180 | pf.mc_properties() 181 | pf.mc_plot_results() 182 | # again, the individual stocks can be added to the plot 183 | pf.plot_stocks() 184 | plt.show() 185 | 186 | # 187 | 188 | print(opt_res) 189 | print() 190 | print(opt_w) 191 | 192 | # 193 | 194 | # # Optimisation overlay 195 | # ## Overlay of Monte Carlo portfolios and Efficient Frontier solutions 196 | 197 | # 198 | 199 | opt_w, opt_res = pf.mc_optimisation(num_trials=5000) 200 | pf.mc_plot_results() 201 | pf.ef_plot_efrontier() 202 | pf.ef.plot_optimal_portfolios() 203 | pf.plot_stocks() 204 | plt.show() 205 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | @-rm -rf *.pyc *.ipynb *.html *.tex *.log *.aux *.out *.tex *.pdf 3 | -------------------------------------------------------------------------------- /finquant/Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | @-rm -rf *.pyc __pycache__ 3 | -------------------------------------------------------------------------------- /finquant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmilthaler/FinQuant/55112a9b3d5182cb291225ea530fce5b403e49e6/finquant/__init__.py -------------------------------------------------------------------------------- /finquant/asset.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a public class ``Asset`` that represents a generic financial asset. 3 | It serves as the parent class for specialized asset classes like ``Stock`` and ``Market`` 4 | in the finquant library. 5 | 6 | An asset is characterized by its historical price data, from which various quantities 7 | such as expected return, volatility, skewness, and kurtosis can be computed. The ``Asset`` 8 | class provides common functionality and attributes that are shared among different types 9 | of assets. 10 | 11 | The specialized asset classes, ``Stock`` and ``Market``, inherit from the ``Asset`` class 12 | and add specific functionality tailored to their respective types. 13 | 14 | """ 15 | 16 | import numpy as np 17 | import pandas as pd 18 | 19 | from finquant.data_types import FLOAT, INT 20 | from finquant.returns import daily_returns, historical_mean_return 21 | from finquant.type_utilities import type_validation 22 | 23 | 24 | class Asset: 25 | """ 26 | Parent class representing a generic financial asset. 27 | 28 | :param data: Historical price data of the asset. 29 | :param name: Name of the asset. 30 | :param asset_type: Type of the asset (e.g., "Stock" or "Market index"). 31 | 32 | The ``Asset`` class provides common functionality and attributes for financial assets. 33 | It represents a generic asset and serves as the parent class for specialized asset classes. 34 | 35 | Attributes: 36 | - ``data`` (``pandas.Series``): Historical price data of the asset. 37 | - ``name`` (``str``): Name of the asset. 38 | - ``asset_type`` (``str``): Type of the asset (e.g., "Stock" or "Market"). 39 | - ``expected_return`` (``float``): Expected return of the asset. 40 | - ``volatility`` (``float``): Volatility of the asset. 41 | - ``skew`` (``float``): Skewness of the asset. 42 | - ``kurtosis`` (``float``): Kurtosis of the asset. 43 | 44 | The ``Asset`` class provides methods to compute various quantities such as expected return, 45 | volatility, skewness, and kurtosis based on the historical price data of the asset. 46 | 47 | """ 48 | 49 | # Attributes: 50 | data: pd.Series 51 | name: str 52 | asset_type: str 53 | expected_return: pd.Series 54 | volatility: FLOAT 55 | skew: FLOAT 56 | kurtosis: FLOAT 57 | 58 | def __init__( 59 | self, data: pd.Series, name: str, asset_type: str = "Market index" 60 | ) -> None: 61 | """ 62 | :param data: Historical price data of the asset. 63 | :param name: Name of the asset 64 | :param asset_type: Type of the asset (e.g., "Stock" or "Market index"), default: "Market index" 65 | """ 66 | self.data = data.astype(np.float64) 67 | self.name = name 68 | # compute expected return and volatility of asset 69 | self.expected_return = self.comp_expected_return() 70 | self.volatility = self.comp_volatility() 71 | self.skew = self._comp_skew() 72 | self.kurtosis = self._comp_kurtosis() 73 | # type of asset 74 | self.asset_type = asset_type 75 | 76 | # functions to compute quantities 77 | def comp_daily_returns(self) -> pd.Series: 78 | """Computes the daily returns (percentage change) of the asset. 79 | See ``finquant.returns.daily_returns``. 80 | """ 81 | return daily_returns(self.data) 82 | 83 | def comp_expected_return(self, freq: INT = 252) -> pd.Series: 84 | """Computes the Expected Return of the asset. 85 | See ``finquant.returns.historical_mean_return``. 86 | 87 | :param freq: Number of trading days in a year, default: 252 88 | :type freq: :py:data:`~.finquant.data_types.INT` 89 | """ 90 | return historical_mean_return(self.data, freq=freq) 91 | 92 | def comp_volatility(self, freq: INT = 252) -> FLOAT: 93 | """Computes the Volatility of the asset. 94 | 95 | :param freq: Number of trading days in a year, default: 252 96 | :type freq: :py:data:`~.finquant.data_types.INT` 97 | 98 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 99 | """ 100 | # Type validations: 101 | type_validation(freq=freq) 102 | volatility: FLOAT = self.comp_daily_returns().std() * np.sqrt(freq) 103 | return volatility 104 | 105 | def _comp_skew(self) -> FLOAT: 106 | """Computes and returns the skewness of the asset.""" 107 | skew: FLOAT = self.data.skew() 108 | return skew 109 | 110 | def _comp_kurtosis(self) -> FLOAT: 111 | """Computes and returns the kurtosis of the asset.""" 112 | kurtosis: FLOAT = self.data.kurt() 113 | return kurtosis 114 | 115 | def properties(self) -> None: 116 | """Nicely prints out the properties of the asset, 117 | with customized messages based on the asset type. 118 | """ 119 | # nicely printing out information and quantities of the asset 120 | string = "-" * 50 121 | string += f"\n{self.asset_type}: {self.name}" 122 | string += f"\nExpected Return: {self.expected_return:0.3f}" 123 | string += f"\nVolatility: {self.volatility:0.3f}" 124 | string += f"\nSkewness: {self.skew:0.5f}" 125 | string += f"\nKurtosis: {self.kurtosis:0.5f}" 126 | string += "\n" + "-" * 50 127 | print(string) 128 | 129 | def __str__(self) -> str: 130 | # print short description 131 | string = f"Contains information about {self.asset_type}: {self.name}." 132 | return string 133 | -------------------------------------------------------------------------------- /finquant/data_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | ``finquant.data_types`` Module 3 | 4 | This module defines type aliases and utility functions for working with arrays, data frames, 5 | and various numeric types in Python, utilizing the 'numpy', 'numpy.typing', and 'pandas' libraries. 6 | 7 | Generic List Element Type 8 | ------------------------- 9 | - ``ELEMENT_TYPE``: A type alias representing a generic element type 10 | 11 | Array/List-Like Types 12 | --------------------- 13 | - ``ARRAY_OR_LIST``: A type alias representing either a NumPy ``ndarray`` or a Python ``List``. 14 | - ``ARRAY_OR_DATAFRAME``: A type alias representing either a NumPy ``ndarray`` or a pandas ``DataFrame``. 15 | - ``ARRAY_OR_SERIES``: A type alias representing either a NumPy ``ndarray`` or a pandas ``Series``. 16 | - ``SERIES_OR_DATAFRAME``: A type alias representing either a pandas ``Series`` or a pandas ``DataFrame``. 17 | 18 | Numeric Types 19 | ------------- 20 | - ``FLOAT``: A type alias representing either a NumPy floating-point number or a Python float. 21 | - ``INT``: A type alias representing either a NumPy integer or a Python int. 22 | - ``NUMERIC``: A type alias representing either an ``INT`` or a ``FLOAT``. 23 | 24 | String/Datetime Types 25 | --------------------- 26 | - ``STRING_OR_DATETIME``: A type alias representing either a Python string or a ``datetime.datetime`` object. 27 | 28 | Dependencies 29 | ------------ 30 | This module requires the following external libraries: 31 | 32 | - ``numpy`` (imported as ``np``) 33 | - ``pandas`` (imported as ``pd``) 34 | 35 | Usage Example 36 | ------------- 37 | 38 | .. code-block:: python 39 | 40 | from finquant.data_types import ARRAY_OR_DATAFRAME, NUMERIC 41 | # Use the defined type aliases 42 | def process_data(data: ARRAY_OR_DATAFRAME) -> FLOAT: 43 | # Process the data and return a floating point number 44 | return 5.0 45 | 46 | """ 47 | # pylint: disable=C0103 48 | 49 | 50 | from datetime import datetime 51 | from typing import Any, KeysView, List, TypeVar, Union 52 | 53 | import numpy as np 54 | import pandas as pd 55 | 56 | # Generic List Element Type 57 | ELEMENT_TYPE = TypeVar("ELEMENT_TYPE") 58 | 59 | # Type Aliases: 60 | ARRAY_OR_LIST = Union[np.ndarray[ELEMENT_TYPE, Any], List[ELEMENT_TYPE]] 61 | ARRAY_OR_DATAFRAME = Union[np.ndarray[ELEMENT_TYPE, Any], pd.DataFrame] 62 | ARRAY_OR_SERIES = Union[np.ndarray[ELEMENT_TYPE, Any], pd.Series] 63 | SERIES_OR_DATAFRAME = Union[pd.Series, pd.DataFrame] 64 | 65 | # To support Dict listkeys: 66 | LIST_DICT_KEYS = Union[ARRAY_OR_LIST[ELEMENT_TYPE], KeysView[ELEMENT_TYPE]] 67 | 68 | # Numeric types 69 | FLOAT = Union[np.floating, float] 70 | INT = Union[np.integer, int] 71 | NUMERIC = Union[INT, FLOAT] 72 | 73 | # String/Datetime types 74 | STRING_OR_DATETIME = Union[str, datetime] 75 | -------------------------------------------------------------------------------- /finquant/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom Exceptions Module 3 | 4 | This module defines custom exception classes that represent various error scenarios 5 | related to financial data retrieval from external APIs. The exceptions are designed 6 | to provide specific context and information about different types of errors that 7 | can occur during the data retrieval process. 8 | 9 | Exceptions: 10 | - InvalidDateFormatError: Raised when an invalid date format is encountered during 11 | date parsing for financial data retrieval. 12 | - QuandlLimitError: Raised when the API limit for Quandl data requests is reached. 13 | - QuandlError: Raised for general errors that occur during Quandl data retrieval. 14 | - YFinanceError: Raised for general errors that occur during YFinance data retrieval. 15 | 16 | Usage: 17 | These custom exceptions can be raised within the respective functions that handle 18 | data retrieval from external APIs, such as Quandl and YFinance. When an exception 19 | is raised, it provides specific information about the error, making it easier to 20 | diagnose and handle exceptional cases during data retrieval operations. 21 | 22 | Example: 23 | try: 24 | # Code that may raise one of the custom exceptions. 25 | except InvalidDateFormatError as exc: 26 | # Handle the invalid date format error here. 27 | except QuandlLimitError as exc: 28 | # Handle the Quandl API limit error here. 29 | except QuandlError as exc: 30 | # Handle other Quandl-related errors here. 31 | except YFinanceError as exc: 32 | # Handle YFinance-related errors here. 33 | 34 | """ 35 | 36 | 37 | class InvalidDateFormatError(Exception): 38 | """ 39 | Exception for Invalid Date Format 40 | 41 | This exception is raised when an invalid date format is encountered during date 42 | parsing for financial data retrieval. It is typically raised when attempting to 43 | convert a string to a datetime object with an incorrect format. 44 | 45 | Example: 46 | try: 47 | start_date = datetime.datetime.strptime("2023/08/01", "%Y-%m-%d") 48 | except ValueError as exc: 49 | raise InvalidDateFormatError("Invalid date format. Use 'YYYY-MM-DD'.") from exc 50 | """ 51 | 52 | 53 | class QuandlLimitError(Exception): 54 | """ 55 | Exception for Quandl API Limit Reached 56 | 57 | This exception is raised when the API limit for Quandl data requests is reached. 58 | It indicates that the rate limit or request quota for the Quandl API has been 59 | exceeded, and no more requests can be made until the limit is reset. 60 | 61 | Example: 62 | try: 63 | resp = quandl.get("GOOG", start_date="2023-08-01", end_date="2023-08-05") 64 | except quandl.errors.QuandlLimit as exc: 65 | raise QuandlLimitError("Quandl API limit reached. Try again later.") from exc 66 | """ 67 | 68 | 69 | class QuandlError(Exception): 70 | """ 71 | Exception for Quandl Data Retrieval Error 72 | 73 | This exception is raised for general errors that occur during Quandl data retrieval. 74 | It can be used to handle any unexpected issues that arise while interacting with 75 | the Quandl API. 76 | 77 | Example: 78 | try: 79 | resp = quandl.get("GOOG", start_date="2023-08-01", end_date="2023-08-05") 80 | except Exception as exc: 81 | raise QuandlError("An error occurred while retrieving data from Quandl.") from exc 82 | """ 83 | 84 | 85 | class YFinanceError(Exception): 86 | """ 87 | Exception for YFinance Data Retrieval Error 88 | 89 | This exception is raised for general errors that occur during YFinance data retrieval. 90 | It can be used to handle any unexpected issues that arise while interacting with 91 | the YFinance library. 92 | 93 | Example: 94 | try: 95 | data = yfinance.download("GOOG", start="2023-08-01", end="2023-08-05") 96 | except Exception as exc: 97 | raise YFinanceError("An error occurred while retrieving data from YFinance.") from exc 98 | """ 99 | -------------------------------------------------------------------------------- /finquant/market.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a public class ``Market`` that represents a market index. 3 | It serves as a specialized asset class within the finquant library. 4 | 5 | A market index represents the performance of a specific market or a segment of the market, 6 | such as the S&P 500 or NASDAQ. The ``Market`` class is designed to hold and calculate quantities 7 | related to a market index, such as expected return, volatility, skewness, and kurtosis. 8 | 9 | The ``Market`` class extends the ``Asset`` class from ``finquant.asset`` and inherits its 10 | common functionality and attributes for financial assets. It provides additional methods 11 | and attributes specific to market indices. 12 | 13 | """ 14 | 15 | 16 | import pandas as pd 17 | 18 | from finquant.asset import Asset 19 | from finquant.returns import daily_returns 20 | 21 | 22 | class Market(Asset): 23 | """ 24 | Class representing a market index. 25 | 26 | :param data: Historical price data of the market index. 27 | 28 | The ``Market`` class extends the ``Asset`` class and represents a specific type of asset, 29 | specifically a market index. 30 | It requires historical price data for the market index to initialize an instance. 31 | 32 | """ 33 | 34 | # Attributes: 35 | daily_returns: pd.DataFrame 36 | 37 | def __init__(self, data: pd.Series) -> None: 38 | """ 39 | :param data: Historical price data of the market index. 40 | """ 41 | super().__init__(data, name=data.name, asset_type="Market index") 42 | self.daily_returns = self.comp_daily_returns() 43 | 44 | # functions to compute quantities 45 | def comp_daily_returns(self) -> pd.Series: 46 | """Computes the daily returns (percentage change) of the market index. 47 | See ``finquant.returns.daily_returns``. 48 | """ 49 | return daily_returns(self.data) 50 | -------------------------------------------------------------------------------- /finquant/minimise_fun.py: -------------------------------------------------------------------------------- 1 | """This module provides a set of function which can used by 2 | scipy.optimize.minimize in order to find the minimal/optimal value. 3 | """ 4 | 5 | 6 | from finquant.data_types import ARRAY_OR_DATAFRAME, ARRAY_OR_SERIES, FLOAT, NUMERIC 7 | from finquant.quants import annualised_portfolio_quantities 8 | from finquant.type_utilities import type_validation 9 | 10 | 11 | def portfolio_volatility( 12 | weights: ARRAY_OR_SERIES[FLOAT], 13 | mean_returns: ARRAY_OR_SERIES[FLOAT], 14 | cov_matrix: ARRAY_OR_DATAFRAME[FLOAT], 15 | ) -> FLOAT: 16 | """Calculates the volatility of a portfolio 17 | 18 | :param weights: An array of weights 19 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 20 | 21 | :param mean_returns: An array of individual expected returns for all stocks 22 | :type mean_returns: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 23 | 24 | :param cov_matrix: Covariance matrix of returns 25 | :type cov_matrix: :py:data:`~.finquant.data_types.ARRAY_OR_DATAFRAME` 26 | 27 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 28 | :return: Annualised volatility 29 | """ 30 | # Type validations: 31 | type_validation(weights=weights, means=mean_returns, cov_matrix=cov_matrix) 32 | return annualised_portfolio_quantities(weights, mean_returns, cov_matrix)[1] 33 | 34 | 35 | def negative_sharpe_ratio( 36 | weights: ARRAY_OR_SERIES[FLOAT], 37 | mean_returns: ARRAY_OR_SERIES[FLOAT], 38 | cov_matrix: ARRAY_OR_DATAFRAME[FLOAT], 39 | risk_free_rate: FLOAT, 40 | ) -> FLOAT: 41 | """Calculates the negative Sharpe ratio of a portfolio 42 | 43 | :param weights: An array of weights 44 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 45 | 46 | :param mean_returns: An array of individual expected returns for all stocks 47 | :type mean_returns: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 48 | 49 | :param cov_matrix: Covariance matrix of returns 50 | :type cov_matrix: :py:data:`~.finquant.data_types.ARRAY_OR_DATAFRAME` 51 | 52 | :param risk_free_rate: Risk free rate 53 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT` 54 | 55 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 56 | :return: Negative sharpe ratio 57 | """ 58 | # Type validations: 59 | type_validation( 60 | weights=weights, 61 | means=mean_returns, 62 | cov_matrix=cov_matrix, 63 | risk_free_rate=risk_free_rate, 64 | ) 65 | sharpe = annualised_portfolio_quantities( 66 | weights, mean_returns, cov_matrix, risk_free_rate=risk_free_rate 67 | )[2] 68 | # to find the maximum Sharpe ratio with scipy.optimize.minimize, 69 | # return the negative of the calculated Sharpe ratio 70 | return -sharpe 71 | 72 | 73 | def portfolio_return( 74 | weights: ARRAY_OR_SERIES[FLOAT], 75 | mean_returns: ARRAY_OR_SERIES[FLOAT], 76 | cov_matrix: ARRAY_OR_DATAFRAME[FLOAT], 77 | ) -> NUMERIC: 78 | """Calculates the expected annualised return of a portfolio 79 | 80 | :param weights: An array of weights 81 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 82 | 83 | :param mean_returns: An array of individual expected returns for all stocks 84 | :type mean_returns: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 85 | 86 | :param cov_matrix: Covariance matrix of returns 87 | :type cov_matrix: :py:data:`~.finquant.data_types.ARRAY_OR_DATAFRAME` 88 | 89 | :rtype: :py:data:`~.finquant.data_types.NUMERIC` 90 | :return: Expected annualised return 91 | """ 92 | # Type validations: 93 | type_validation(weights=weights, means=mean_returns, cov_matrix=cov_matrix) 94 | return annualised_portfolio_quantities(weights, mean_returns, cov_matrix)[0] 95 | -------------------------------------------------------------------------------- /finquant/monte_carlo.py: -------------------------------------------------------------------------------- 1 | """The module provides a class ``MonteCarlo`` which is an implementation of the 2 | Monte Carlo method and a class ``MonteCarloOpt`` which allows the user to perform a 3 | Monte Carlo run to find optimised financial portfolios, given an initial portfolio. 4 | """ 5 | 6 | 7 | from typing import Any, Callable, Dict, Optional, Tuple 8 | 9 | import matplotlib.pylab as plt 10 | import numpy as np 11 | import pandas as pd 12 | 13 | from finquant.data_types import FLOAT, INT 14 | from finquant.quants import annualised_portfolio_quantities 15 | from finquant.type_utilities import type_validation 16 | 17 | 18 | class MonteCarlo: 19 | """An object to perform a Monte Carlo run/simulation.""" 20 | 21 | # Attributes: 22 | num_trials: int 23 | 24 | def __init__(self, num_trials: int = 1000): 25 | """ 26 | :param num_trials: Number of iterations of the Monte Carlo run/simulation, default: 1000 27 | """ 28 | self.num_trials = num_trials 29 | 30 | def run( 31 | self, fun: Callable[..., Any], **kwargs: Dict[str, Any] 32 | ) -> np.ndarray[np.float64, Any]: 33 | """ 34 | :param fun: Function to call at each iteration of the Monte Carlo run. 35 | 36 | :param kwargs: (optional) Additional arguments that are passed to ``fun``. 37 | 38 | :result: Array of quantities returned from ``fun`` at each iteration. 39 | """ 40 | # Type validations: 41 | type_validation(fun=fun) 42 | result = [] 43 | for _ in range(self.num_trials): 44 | res = fun(**kwargs) 45 | result.append(res) 46 | return np.asarray(result, dtype=np.ndarray) 47 | 48 | 49 | class MonteCarloOpt(MonteCarlo): 50 | """An object to perform a Monte Carlo run/simulation for finding 51 | optimised financial portfolios. 52 | 53 | Inherits from `MonteCarlo`. 54 | """ 55 | 56 | # Attributes: 57 | returns: pd.DataFrame 58 | risk_free_rate: FLOAT 59 | freq: INT 60 | initial_weights: Optional[np.ndarray[np.float64, Any]] 61 | 62 | def __init__( 63 | self, 64 | returns: pd.DataFrame, 65 | num_trials: int = 1000, 66 | risk_free_rate: FLOAT = 0.005, 67 | freq: INT = 252, 68 | initial_weights: Optional[np.ndarray[np.float64, Any]] = None, 69 | ) -> None: 70 | """ 71 | :param returns: DataFrame of returns of stocks 72 | Note: If applicable, the given returns should be computed with the same risk free rate 73 | and time window/frequency (arguments ``risk_free_rate`` and ``freq`` as passed in here. 74 | :param num_trials: Number of portfolios to be computed, 75 | each with a random distribution of weights/allocation in each stock, default: 1000 76 | :param risk_free_rate: Risk free rate as required for the Sharpe Ratio, default: 0.005 77 | :param freq: Number of trading days in a year, default: 252 78 | :param initial_weights: Weights of initial/given portfolio, only used to plot a marker for the 79 | initial portfolio in the optimisation plot, default: ``None`` 80 | """ 81 | # Type validations: 82 | type_validation( 83 | returns_df=returns, 84 | num_trials=num_trials, 85 | risk_free_rate=risk_free_rate, 86 | freq=freq, 87 | initial_weights=initial_weights, 88 | ) 89 | self.returns = returns 90 | self.num_trials = num_trials 91 | self.risk_free_rate = risk_free_rate 92 | self.freq = freq 93 | self.initial_weights: np.ndarray[float, Any] = initial_weights 94 | # initiate super class 95 | super().__init__(num_trials=self.num_trials) 96 | # setting additional variables 97 | self.num_stocks = len(self.returns.columns) 98 | self.return_means = self.returns.mean() 99 | self.cov_matrix = self.returns.cov() 100 | # setting up variables for results 101 | self.df_weights = None 102 | self.df_results = None 103 | self.opt_weights = None 104 | self.opt_results = None 105 | 106 | def _random_weights( 107 | self, 108 | ) -> Tuple[np.ndarray[np.float64, Any], np.ndarray[np.float64, Any]]: 109 | """Computes random weights for the stocks of a portfolio and the 110 | corresponding Expected Return, Volatility and Sharpe Ratio. 111 | 112 | :result: Tuple of (weights (array), and array of expected return, volatility, sharpe ratio) 113 | """ 114 | # select random weights for portfolio 115 | weights: np.ndarray[np.float64, Any] = np.array( 116 | np.random.random(self.num_stocks), dtype=np.float64 117 | ) 118 | # rebalance weights 119 | weights = weights / np.sum(weights) 120 | # compute portfolio return and volatility 121 | portfolio_values: np.ndarray[np.float64, Any] = np.array( 122 | annualised_portfolio_quantities( 123 | weights, 124 | self.return_means, 125 | self.cov_matrix, 126 | self.risk_free_rate, 127 | self.freq, 128 | ), 129 | dtype=np.float64, 130 | ) 131 | return (weights, portfolio_values) 132 | 133 | def _random_portfolios(self) -> Tuple[pd.DataFrame, pd.DataFrame]: 134 | """Performs a Monte Carlo run and gets a list of random portfolios 135 | and their corresponding quantities (Expected Return, Volatility, 136 | Sharpe Ratio). 137 | 138 | :return: 139 | :df_weights: DataFrame, holds the weights for each randomly generated portfolio 140 | :df_results: DataFrame, holds Expected Annualised Return, Volatility and 141 | Sharpe Ratio of each randomly generated portfolio 142 | """ 143 | # run Monte Carlo to get random weights and corresponding quantities 144 | res = self.run(self._random_weights) 145 | # convert to pandas.DataFrame: 146 | weights_columns = list(self.returns.columns) 147 | result_columns = ["Expected Return", "Volatility", "Sharpe Ratio"] 148 | df_weights = pd.DataFrame( 149 | data=res[:, 0].tolist(), columns=weights_columns 150 | ).astype(np.float64) 151 | df_results = pd.DataFrame( 152 | data=res[:, 1].tolist(), columns=result_columns 153 | ).astype(np.float64) 154 | return (df_weights, df_results) 155 | 156 | def optimisation(self) -> Tuple[pd.DataFrame, pd.DataFrame]: 157 | """Optimisation of the portfolio by performing a Monte Carlo 158 | simulation. 159 | 160 | :return: 161 | :opt_w: DataFrame with optimised investment strategies for maximum 162 | Sharpe Ratio and minimum volatility. 163 | :opt_res: DataFrame with Expected Return, Volatility and Sharpe Ratio 164 | for portfolios with minimum Volatility and maximum Sharpe Ratio. 165 | """ 166 | # perform Monte Carlo run and get weights and results 167 | df_weights, df_results = self._random_portfolios() 168 | # finding portfolios with the minimum volatility and maximum 169 | # Sharpe ratio 170 | index_min_volatility = df_results["Volatility"].idxmin() 171 | index_max_sharpe = df_results["Sharpe Ratio"].idxmax() 172 | # storing optimal results to DataFrames 173 | opt_w: pd.DataFrame = pd.DataFrame( 174 | [df_weights.iloc[index_min_volatility], df_weights.iloc[index_max_sharpe]], 175 | index=["Min Volatility", "Max Sharpe Ratio"], 176 | ) 177 | opt_res: pd.DataFrame = pd.DataFrame( 178 | [df_results.iloc[index_min_volatility], df_results.iloc[index_max_sharpe]], 179 | index=["Min Volatility", "Max Sharpe Ratio"], 180 | ) 181 | # setting instance variables: 182 | self.df_weights = df_weights.astype(np.float64) 183 | self.df_results = df_results.astype(np.float64) 184 | self.opt_weights = opt_w.astype(np.float64) 185 | self.opt_results = opt_res.astype(np.float64) 186 | return opt_w, opt_res 187 | 188 | def plot_results(self) -> None: 189 | """Plots the results of the Monte Carlo run, with all of the 190 | randomly generated weights/portfolios, as well as markers 191 | for the portfolios with the minimum Volatility and maximum 192 | Sharpe Ratio. 193 | """ 194 | if ( 195 | self.df_results is None 196 | or self.df_weights is None 197 | or self.opt_weights is None 198 | or self.opt_results is None 199 | ): 200 | raise ValueError( 201 | "Error: Cannot plot, run the Monte Carlo " + "optimisation first." 202 | ) 203 | # create scatter plot coloured by Sharpe Ratio 204 | plt.scatter( 205 | self.df_results["Volatility"], 206 | self.df_results["Expected Return"], 207 | c=self.df_results["Sharpe Ratio"], 208 | cmap="RdYlBu", 209 | s=10, 210 | label=None, 211 | ) 212 | cbar = plt.colorbar() 213 | # mark in green the minimum volatility 214 | plt.scatter( 215 | self.opt_results.loc["Min Volatility"]["Volatility"], 216 | self.opt_results.loc["Min Volatility"]["Expected Return"], 217 | marker="^", 218 | color="g", 219 | s=100, 220 | label="min Volatility", 221 | ) 222 | # mark in red the highest sharpe ratio 223 | plt.scatter( 224 | self.opt_results.loc["Max Sharpe Ratio"]["Volatility"], 225 | self.opt_results.loc["Max Sharpe Ratio"]["Expected Return"], 226 | marker="^", 227 | color="r", 228 | s=100, 229 | label="max Sharpe Ratio", 230 | ) 231 | # also set marker for initial portfolio, if weights were given 232 | if self.initial_weights is not None: 233 | # computed expected return and volatility of initial portfolio 234 | initial_values = annualised_portfolio_quantities( 235 | self.initial_weights, 236 | self.return_means, 237 | self.cov_matrix, 238 | self.risk_free_rate, 239 | self.freq, 240 | ) 241 | initial_return = initial_values[0] 242 | initial_volatility = initial_values[1] 243 | plt.scatter( 244 | initial_volatility, 245 | initial_return, 246 | marker="^", 247 | color="k", 248 | s=100, 249 | label="Initial Portfolio", 250 | ) 251 | plt.title( 252 | "Monte Carlo simulation to optimise the portfolio based " 253 | + "on the Efficient Frontier" 254 | ) 255 | plt.xlabel("Volatility [period=" + str(self.freq) + "]") 256 | plt.ylabel("Expected Return [period=" + str(self.freq) + "]") 257 | cbar.ax.set_ylabel("Sharpe Ratio [period=" + str(self.freq) + "]", rotation=90) 258 | plt.legend() 259 | 260 | def properties(self) -> None: 261 | """Prints out the properties of the Monte Carlo optimisation.""" 262 | if self.opt_weights is None or self.opt_results is None: 263 | print( 264 | "Error: Optimal weights and/or results are not computed. Please perform a Monte Carlo run first." 265 | ) 266 | else: 267 | # print out results 268 | opt_vals = ["Min Volatility", "Max Sharpe Ratio"] 269 | string = "" 270 | for val in opt_vals: 271 | string += "-" * 70 272 | string += f"\nOptimised portfolio for {val.replace('Min', 'Minimum').replace('Max', 'Maximum')}" 273 | string += f"\n\nTime period: {self.freq} days" 274 | string += f"\nExpected return: {self.opt_results.loc[val]['Expected Return']:0.3f}" 275 | string += ( 276 | f"\nVolatility: {self.opt_results.loc[val]['Volatility']:0.3f}" 277 | ) 278 | string += ( 279 | f"\nSharpe Ratio: {self.opt_results.loc[val]['Sharpe Ratio']:0.3f}" 280 | ) 281 | string += "\n\nOptimal weights:" 282 | string += "\n" + str( 283 | self.opt_weights.loc[val] 284 | .to_frame() 285 | .transpose() 286 | .rename(index={val: "Allocation"}) 287 | ) 288 | string += "\n" 289 | string += "-" * 70 290 | print(string) 291 | -------------------------------------------------------------------------------- /finquant/moving_average.py: -------------------------------------------------------------------------------- 1 | """The module provides functions to compute and visualise: 2 | 3 | - *Simple Moving Averages*, 4 | - *Exponential Moving Averages*, 5 | - a *band* of *Moving Averages* (simple or exponential), and 6 | - *Bollinger Bands*. 7 | """ 8 | 9 | 10 | from typing import Callable, List 11 | 12 | import matplotlib.pyplot as plt 13 | import numpy as np 14 | import pandas as pd 15 | 16 | from finquant.data_types import SERIES_OR_DATAFRAME 17 | from finquant.type_utilities import type_validation 18 | 19 | 20 | def compute_ma( 21 | data: SERIES_OR_DATAFRAME, 22 | fun: Callable[[SERIES_OR_DATAFRAME, int], pd.Series], 23 | spans: List[int], 24 | plot: bool = True, 25 | ) -> pd.DataFrame: 26 | """Computes a band of moving averages (sma or ema, depends on the input argument 27 | `fun`) for a number of different time windows. If `plot` is `True`, it also 28 | computes and sets markers for buy/sell signals based on crossovers of the Moving 29 | Averages with the shortest/longest spans. 30 | 31 | :param data: A series/dataframe of daily stock prices (if DataFrame, 32 | only one column is expected) 33 | :type data: :py:data:`~.finquant.data_types.SERIES_OR_DATAFRAME` 34 | :param fun: Function that computes a moving average, e.g. ```sma``` (simple) or 35 | ```ema``` (exponential). 36 | :param spans: List of integers, time windows to compute the Moving Average on. 37 | :param plot: boolean, whether to plot the moving averages 38 | and buy/sell signals based on crossovers of shortest and longest 39 | moving average, or not, default: True 40 | 41 | :return: Moving averages of given data. 42 | """ 43 | # Type validations: 44 | type_validation(data=data, fun=fun, spans=spans, plot=plot) 45 | m_a = data.copy(deep=True) 46 | # converting data to pd.DataFrame if it is a pd.Series (for subsequent function calls): 47 | if isinstance(m_a, pd.Series): 48 | m_a = m_a.to_frame() 49 | # compute moving averages 50 | for span in spans: 51 | m_a[str(span) + "d"] = fun(data, span) 52 | if plot: 53 | fig = plt.figure() 54 | axis = fig.add_subplot(111) 55 | # plot moving averages 56 | m_a.plot(ax=axis) 57 | # Create buy/sell signals of shortest and longest span 58 | minspan = min(spans) 59 | minlabel = str(minspan) + "d" 60 | maxspan = max(spans) 61 | maxlabel = str(maxspan) + "d" 62 | signals = m_a.copy(deep=True) 63 | signals["diff"] = 0.0 64 | signals["diff"][minspan:] = np.where( 65 | m_a[minlabel][minspan:] > m_a[maxlabel][minspan:], 1.0, 0.0 66 | ) 67 | # Generate trading orders 68 | signals["signal"] = signals["diff"].diff() 69 | # marker for buy signal 70 | axis.plot( 71 | signals.loc[signals["signal"] == 1.0].index.values, 72 | signals[minlabel][signals["signal"] == 1.0].values, 73 | marker="^", 74 | markersize=10, 75 | color="r", 76 | label="buy signal", 77 | ) 78 | # marker for sell signal 79 | axis.plot( 80 | signals.loc[signals["signal"] == -1.0].index.values, 81 | signals[minlabel][signals["signal"] == -1.0].values, 82 | marker="v", 83 | markersize=10, 84 | color="b", 85 | label="sell signal", 86 | ) 87 | # title 88 | title = "Band of Moving Averages (" + str(fun.__name__) + ")" 89 | plt.title(title) 90 | # legend 91 | plt.legend(ncol=2) 92 | # axis labels 93 | plt.xlabel(data.index.name) 94 | plt.ylabel("Price") 95 | return m_a 96 | 97 | 98 | def sma(data: pd.DataFrame, span: int = 100) -> pd.DataFrame: 99 | """Computes and returns the simple moving average. 100 | 101 | Note: the moving average is computed on all columns. 102 | 103 | :param data: A dataframe of daily stock prices 104 | :param span: Number of days/values over which the average is computed, default: 100 105 | 106 | :return: Simple moving average 107 | """ 108 | # Type validations: 109 | type_validation(data=data, span=span) 110 | return data.rolling(window=span, center=False).mean() 111 | 112 | 113 | def ema(data: pd.DataFrame, span: int = 100) -> pd.DataFrame: 114 | """Computes and returns the exponential moving average. 115 | 116 | Note: the moving average is computed on all columns. 117 | 118 | :param data: A dataframe of daily stock prices 119 | :param span: Number of days/values over which the average is computed, default: 100 120 | 121 | :return: Exponential moving average 122 | """ 123 | # Type validations: 124 | type_validation(data=data, span=span) 125 | return data.ewm(span=span, adjust=False, min_periods=span).mean() 126 | 127 | 128 | def sma_std(data: pd.DataFrame, span: int = 100) -> pd.DataFrame: 129 | """Computes and returns the standard deviation of the simple moving 130 | average. 131 | 132 | :param data: A dataframe of daily stock prices 133 | :param span: Number of days/values over which the average is computed, default: 100 134 | 135 | :return: Standard deviation of simple moving average 136 | """ 137 | # Type validations: 138 | type_validation(data=data, span=span) 139 | return data.rolling(window=span, center=False).std() 140 | 141 | 142 | def ema_std(data: pd.DataFrame, span: int = 100) -> pd.DataFrame: 143 | """Computes and returns the standard deviation of the exponential 144 | moving average. 145 | 146 | :param data: A dataframe of daily stock prices 147 | :param span: Number of days/values over which the average is computed, default: 100 148 | 149 | :return: Standard deviation of exponential moving average 150 | """ 151 | # Type validations: 152 | type_validation(data=data, span=span) 153 | return data.ewm(span=span, adjust=False, min_periods=span).std() 154 | 155 | 156 | def plot_bollinger_band( 157 | data: pd.DataFrame, 158 | fun: Callable[[pd.DataFrame, int], pd.DataFrame], 159 | span: int = 100, 160 | ) -> None: 161 | """Computes and visualises a Bolling Band. 162 | 163 | :param data: A dataframe of daily stock prices 164 | :param fun: function that computes a moving average, e.g. ``sma`` (simple) or 165 | ``ema`` (exponential). 166 | :param span: Number of days/values over which the average is computed, default: 100 167 | """ 168 | # Type validations: 169 | type_validation(data=data, fun=fun, span=span) 170 | # special requirement for dataframe "data": 171 | if isinstance(data, pd.DataFrame) and not len(data.columns.values) == 1: 172 | raise ValueError("data is expected to have only one column.") 173 | # converting data to pd.DataFrame if it is a pd.Series (for subsequent function calls): 174 | if isinstance(data, pd.Series): 175 | data = data.to_frame() 176 | # compute moving average 177 | m_a = compute_ma(data, fun, [span], plot=False) 178 | # create dataframes for bollinger band object and standard 179 | # deviation 180 | bol = m_a.copy(deep=True) 181 | std = m_a.copy(deep=True) 182 | # get column label 183 | collabel = data.columns.values[0] 184 | # get standard deviation 185 | if fun is sma: 186 | std[str(span) + "d std"] = sma_std(data[collabel], span=span) 187 | elif fun is ema: 188 | std[str(span) + "d std"] = ema_std(data[collabel], span=span) 189 | # compute upper and lower band 190 | bol["Lower Band"] = bol[str(span) + "d"] - (std[str(span) + "d std"] * 2) 191 | bol["Upper Band"] = bol[str(span) + "d"] + (std[str(span) + "d std"] * 2) 192 | # plot 193 | fig = plt.figure() 194 | axis = fig.add_subplot(111) 195 | # bollinger band 196 | axis.fill_between( 197 | data.index.values, 198 | bol["Upper Band"], 199 | bol["Lower Band"], 200 | color="darkgrey", 201 | label="Bollinger Band", 202 | ) 203 | # plot data and moving average 204 | bol[collabel].plot(ax=axis) 205 | bol[str(span) + "d"].plot(ax=axis) 206 | # title 207 | title = ( 208 | "Bollinger Band of +/- 2$\\sigma$, Moving Average of " 209 | + str(fun.__name__) 210 | + " over " 211 | + str(span) 212 | + " days" 213 | ) 214 | plt.title(title) 215 | # legend 216 | plt.legend() 217 | # axis labels 218 | plt.xlabel(data.index.name) 219 | plt.ylabel("Price") 220 | -------------------------------------------------------------------------------- /finquant/quants.py: -------------------------------------------------------------------------------- 1 | """The module provides functions to compute quantities relevant to financial 2 | portfolios, e.g. a weighted average, which is the expected value/return, a 3 | weighted standard deviation (volatility), and the Sharpe ratio. 4 | """ 5 | 6 | 7 | from typing import Optional, Tuple 8 | 9 | import numpy as np 10 | import pandas as pd 11 | from scipy.stats import norm 12 | 13 | from finquant.data_types import ARRAY_OR_DATAFRAME, ARRAY_OR_SERIES, FLOAT, INT, NUMERIC 14 | from finquant.returns import weighted_mean_daily_returns 15 | from finquant.type_utilities import type_validation 16 | 17 | 18 | def weighted_mean( 19 | means: ARRAY_OR_SERIES[FLOAT], weights: ARRAY_OR_SERIES[FLOAT] 20 | ) -> FLOAT: 21 | """Computes the weighted mean/average, or in the case of a 22 | financial portfolio, it can be used for the Expected Return 23 | of said portfolio. 24 | 25 | :param means: An array representing mean/average values 26 | :type means: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 27 | 28 | :param weights: An array representing weights 29 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 30 | 31 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 32 | :return: The weighted mean as a floating point number: ``np.sum(means*weights)`` 33 | """ 34 | # Type validations: 35 | type_validation(means=means, weights=weights) 36 | weighted_mu: FLOAT = float(np.sum(means * weights)) 37 | return weighted_mu 38 | 39 | 40 | def weighted_std( 41 | cov_matrix: ARRAY_OR_DATAFRAME[FLOAT], weights: ARRAY_OR_SERIES[FLOAT] 42 | ) -> FLOAT: 43 | """Computes the weighted standard deviation, or Volatility of 44 | a portfolio, which contains several stocks. 45 | 46 | :param cov_matrix: Covariance matrix 47 | :type cov_matrix: :py:data:`~.finquant.data_types.ARRAY_OR_DATAFRAME` 48 | 49 | :param weights: An array representing weights 50 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 51 | 52 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 53 | :return: Weighted sigma (standard deviation) as a floating point number: 54 | ``np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))`` 55 | """ 56 | # Type validations: 57 | type_validation(cov_matrix=cov_matrix, weights=weights) 58 | weighted_sigma: FLOAT = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights))) 59 | return weighted_sigma 60 | 61 | 62 | def sharpe_ratio( 63 | exp_return: FLOAT, volatility: FLOAT, risk_free_rate: FLOAT = 0.005 64 | ) -> FLOAT: 65 | """Computes the Sharpe Ratio 66 | 67 | :param exp_return: Expected Return of a portfolio 68 | :type exp_return: :py:data:`~.finquant.data_types.FLOAT` 69 | 70 | :param volatility: Volatility of a portfolio 71 | :type volatility: :py:data:`~.finquant.data_types.FLOAT` 72 | 73 | :param risk_free_rate: Risk free rate 74 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005 75 | 76 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 77 | :return: Sharpe Ratio as a floating point number: 78 | ``(exp_return-risk_free_rate)/float(volatility)`` 79 | """ 80 | # Type validations: 81 | type_validation( 82 | expected_return=exp_return, volatility=volatility, risk_free_rate=risk_free_rate 83 | ) 84 | res_sharpe_ratio: FLOAT = (exp_return - risk_free_rate) / float(volatility) 85 | return res_sharpe_ratio 86 | 87 | 88 | def sortino_ratio( 89 | exp_return: FLOAT, downs_risk: FLOAT, risk_free_rate: FLOAT = 0.005 90 | ) -> FLOAT: 91 | """Computes the Sortino Ratio. 92 | 93 | :param exp_return: Expected Return of a portfolio 94 | :type exp_return: :py:data:`~.finquant.data_types.FLOAT` 95 | 96 | :param downs_risk: Downside Risk of a portfolio 97 | :type exp_return: :py:data:`~.finquant.data_types.FLOAT` 98 | 99 | :param risk_free_rate: Risk free rate 100 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005 101 | 102 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 103 | :return: Sortino Ratio as a floating point number: 104 | ``(exp_return - risk_free_rate) / float(downs_risk)`` 105 | """ 106 | # Type validations: 107 | type_validation( 108 | expected_return=exp_return, 109 | downside_risk=downs_risk, 110 | risk_free_rate=risk_free_rate, 111 | ) 112 | if float(downs_risk) == 0: 113 | return np.nan 114 | else: 115 | return (exp_return - risk_free_rate) / float(downs_risk) 116 | 117 | 118 | def treynor_ratio( 119 | exp_return: FLOAT, beta: Optional[FLOAT], risk_free_rate: FLOAT = 0.005 120 | ) -> Optional[FLOAT]: 121 | """Computes the Treynor Ratio. 122 | 123 | :param exp_return: Expected Return of a portfolio 124 | :type exp_return: :py:data:`~.finquant.data_types.FLOAT` 125 | 126 | :param beta: Beta parameter of a portfolio 127 | :type beta: :py:data:`~.finquant.data_types.FLOAT` 128 | 129 | :param risk_free_rate: Risk free rate 130 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005 131 | 132 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 133 | :return: Treynor Ratio as a floating point number: 134 | ``(exp_return - risk_free_rate) / beta`` 135 | """ 136 | # Type validations: 137 | type_validation( 138 | expected_return=exp_return, 139 | beta_parameter=beta, 140 | risk_free_rate=risk_free_rate, 141 | ) 142 | if beta is None: 143 | return None 144 | else: 145 | res_treynor_ratio: FLOAT = (exp_return - risk_free_rate) / beta 146 | return res_treynor_ratio 147 | 148 | 149 | def downside_risk( 150 | data: pd.DataFrame, weights: ARRAY_OR_SERIES[FLOAT], risk_free_rate: FLOAT = 0.005 151 | ) -> FLOAT: 152 | """Computes the downside risk (target downside deviation of returns). 153 | 154 | :param data: A dataframe of daily stock prices 155 | 156 | :param weights: Downside Risk of a portfolio 157 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 158 | 159 | :param risk_free_rate: Risk free rate 160 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005 161 | 162 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 163 | :return: Target downside deviation 164 | ``np.sqrt(np.mean(np.minimum(0, wtd_daily_mean - risk_free_rate) ** 2))`` 165 | """ 166 | # Type validations: 167 | type_validation(data=data, weights=weights, risk_free_rate=risk_free_rate) 168 | wtd_daily_mean = weighted_mean_daily_returns(data, weights) 169 | return float(np.sqrt(np.mean(np.minimum(0, wtd_daily_mean - risk_free_rate) ** 2))) 170 | 171 | 172 | def value_at_risk( 173 | investment: NUMERIC, mu: FLOAT, sigma: FLOAT, conf_level: FLOAT = 0.95 174 | ) -> FLOAT: 175 | """Computes and returns the expected value at risk of an investment/assets. 176 | 177 | :param investment: Total value of the investment 178 | :type investment: :py:data:`~.finquant.data_types.NUMERIC` 179 | 180 | :param mu: Average/mean return of the investment 181 | :type mu: :py:data:`~.finquant.data_types.FLOAT` 182 | 183 | :param sigma: Standard deviation of the investment 184 | :type sigma: :py:data:`~.finquant.data_types.FLOAT` 185 | 186 | :param conf_level: Confidence level of the VaR 187 | :type conf_level: :py:data:`~.finquant.data_types.FLOAT`, default: 0.95 188 | 189 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 190 | :return: Value at Risk (VaR) of the investment: ``investment*(mu-sigma*norm.ppf(1-conf_level))`` 191 | """ 192 | # Type validations: 193 | type_validation(investment=investment, mu=mu, sigma=sigma, conf_level=conf_level) 194 | if conf_level >= 1 or conf_level <= 0: 195 | raise ValueError("confidence level is expected to be between 0 and 1.") 196 | res_value_at_risk: FLOAT = investment * (mu - sigma * norm.ppf(1 - conf_level)) 197 | return res_value_at_risk 198 | 199 | 200 | def annualised_portfolio_quantities( 201 | weights: ARRAY_OR_SERIES[FLOAT], 202 | means: ARRAY_OR_SERIES[FLOAT], 203 | cov_matrix: ARRAY_OR_DATAFRAME[FLOAT], 204 | risk_free_rate: FLOAT = 0.005, 205 | freq: INT = 252, 206 | ) -> Tuple[FLOAT, FLOAT, FLOAT]: 207 | """Computes and returns the expected annualised return, volatility 208 | and Sharpe Ratio of a portfolio. 209 | 210 | :param weights: An array of weights 211 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 212 | 213 | :param means: An array of mean/average values 214 | :type means: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 215 | 216 | :param cov_matrix: Covariance matrix 217 | :type cov_matrix: :py:data:`~.finquant.data_types.ARRAY_OR_DATAFRAME` 218 | 219 | :param risk_free_rate: Risk free rate 220 | :type risk_free_rate: :py:data:`~.finquant.data_types.FLOAT`, default: 0.005 221 | 222 | :param freq: Number of trading days in a year 223 | :type freq: :py:data:`~.finquant.data_types.INT`, default: 252 224 | 225 | :rtype: Tuple[:py:data:`~.finquant.data_types.FLOAT`, 226 | :py:data:`~.finquant.data_types.FLOAT`, 227 | :py:data:`~.finquant.data_types.FLOAT`] 228 | :return: Tuple of Expected Return, Volatility, Sharpe Ratio 229 | """ 230 | # Type validations: 231 | type_validation( 232 | weights=weights, 233 | means=means, 234 | cov_matrix=cov_matrix, 235 | risk_free_rate=risk_free_rate, 236 | freq=freq, 237 | ) 238 | expected_return = weighted_mean(means, weights) * freq 239 | volatility = weighted_std(cov_matrix, weights) * np.sqrt(freq) 240 | sharpe = sharpe_ratio(expected_return, volatility, risk_free_rate) 241 | return (expected_return, volatility, sharpe) 242 | -------------------------------------------------------------------------------- /finquant/returns.py: -------------------------------------------------------------------------------- 1 | """The module provides functions to compute different kinds of returns of stocks.""" 2 | 3 | 4 | from typing import Any 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | from finquant.data_types import ( 10 | ARRAY_OR_SERIES, 11 | FLOAT, 12 | INT, 13 | NUMERIC, 14 | SERIES_OR_DATAFRAME, 15 | ) 16 | from finquant.type_utilities import type_validation 17 | 18 | 19 | def cumulative_returns(data: pd.DataFrame, dividend: NUMERIC = 0) -> pd.DataFrame: 20 | """Returns DataFrame with cumulative returns 21 | 22 | :math:`\\displaystyle R = \\dfrac{\\text{price}_{t_i} - \\text{price}_{t_0} + \\text{dividend}} 23 | {\\text{price}_{t_0}}` 24 | 25 | :param data: A dataframe of daily stock prices 26 | 27 | :param dividend: Paid dividend 28 | :type dividend: :py:data:`~.finquant.data_types.NUMERIC`, default: 0 29 | 30 | :return: A dataframe of cumulative returns of given stock prices. 31 | """ 32 | # Type validations: 33 | type_validation(data=data, dividend=dividend) 34 | data = data.dropna(axis=0, how="any") 35 | return ((data - data.iloc[0] + dividend) / data.iloc[0]).astype(np.float64) 36 | 37 | 38 | def daily_returns(data: pd.DataFrame) -> pd.DataFrame: 39 | """Returns DataFrame with daily returns (percentage change) 40 | 41 | :math:`\\displaystyle R = \\dfrac{\\text{price}_{t_i} - \\text{price}_{t_{i-1}}}{\\text{price}_{t_{i-1}}}` 42 | 43 | :param data: A dataframe of daily stock prices 44 | 45 | :return: A dataframe of daily percentage change of returns of given stock prices. 46 | """ 47 | # Type validations: 48 | type_validation(data=data) 49 | return ( 50 | data.pct_change() 51 | .dropna(how="all") 52 | .replace([np.inf, -np.inf], np.nan) 53 | .astype(np.float64) 54 | ) 55 | 56 | 57 | def weighted_mean_daily_returns( 58 | data: pd.DataFrame, weights: ARRAY_OR_SERIES[FLOAT] 59 | ) -> np.ndarray[FLOAT, Any]: 60 | """Returns DataFrame with the daily weighted mean returns 61 | 62 | :param data: A dataframe of daily stock prices 63 | 64 | :param weights: An array representing weights 65 | :type weights: :py:data:`~.finquant.data_types.ARRAY_OR_SERIES` 66 | 67 | :return: An array of weighted mean daily percentage change of Returns 68 | """ 69 | # Type validations: 70 | type_validation(data=data, weights=weights) 71 | res: np.ndarray[FLOAT, Any] = np.dot(daily_returns(data), weights) 72 | return res 73 | 74 | 75 | def daily_log_returns(data: pd.DataFrame) -> pd.DataFrame: 76 | """ 77 | Returns DataFrame with daily log returns 78 | 79 | :math:`R_{\\log} = \\log\\left(1 + \\dfrac{\\text{price}_{t_i} - \\text{price}_{t_{i-1}}} 80 | {\\text{price}_{t_{i-1}}}\\right)` 81 | 82 | :param data: A dataframe of daily stock prices 83 | 84 | :return: A dataframe of daily log returns 85 | """ 86 | # Type validations: 87 | type_validation(data=data) 88 | return np.log(1 + daily_returns(data)).dropna(how="all").astype(np.float64) 89 | 90 | 91 | def historical_mean_return(data: SERIES_OR_DATAFRAME, freq: INT = 252) -> pd.Series: 92 | """Returns the *mean return* based on historical stock price data. 93 | 94 | :param data: A dataframe of daily stock prices 95 | :type data: :py:data:`~.finquant.data_types.SERIES_OR_DATAFRAME` 96 | 97 | :param freq: Number of trading days in a year 98 | :type freq: :py:data:`~.finquant.data_types.INT`, default: 252 99 | 100 | :return: A series of historical mean returns 101 | """ 102 | # Type validations: 103 | type_validation(data=data, freq=freq) 104 | return daily_returns(data).mean() * freq 105 | -------------------------------------------------------------------------------- /finquant/stock.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a public class ``Stock`` that represents a single stock or fund. 3 | Instances of this class are used within the ``Portfolio`` class (provided in ``finquant.portfolio``). 4 | 5 | The ``Stock`` class is designed to hold and calculate quantities related to a single stock or fund. 6 | To initialize an instance of ``Stock``, it requires the following information: 7 | 8 | - ``investmentinfo``: Information about the stock or fund provided as a ``pandas.DataFrame``. 9 | The required column labels are ``Name`` and ``Allocation`` for the stock/fund name and allocation, 10 | respectively. However, the DataFrame can contain more information beyond these columns, 11 | such as year, strategy, currency (CCY), etc. 12 | 13 | - ``data``: Historical price data for the stock or fund provided as a ``pandas.Series``. 14 | The data must contain `` - Adj. Close``, which represents the closing price used to 15 | compute the return on investment. 16 | 17 | The ``Stock`` class computes various quantities related to the stock or fund, such as expected return, 18 | volatility, skewness, and kurtosis. It also provides functionality to calculate the beta parameter 19 | using the CAPM (Capital Asset Pricing Model) and the R squared value of the stock . 20 | 21 | The ``Stock`` class inherits from the ``Asset`` class in ``finquant.asset``, which provides common 22 | functionality and attributes for financial assets. 23 | 24 | """ 25 | 26 | from typing import Optional 27 | 28 | import numpy as np 29 | import pandas as pd 30 | from sklearn.metrics import r2_score 31 | 32 | from finquant.asset import Asset 33 | from finquant.data_types import FLOAT 34 | from finquant.type_utilities import type_validation 35 | 36 | 37 | class Stock(Asset): 38 | """Class that contains information about a stock/fund. 39 | 40 | :param investmentinfo: Investment information of a stock. 41 | :param data: Historical price data of a stock. 42 | 43 | The ``Stock`` class extends the ``Asset`` class and represents a specific type of asset, 44 | namely a stock within a portfolio. 45 | It requires investment information and historical price data for the stock to initialize an instance. 46 | 47 | In addition to the attributes inherited from the ``Asset`` class, the ``Stock`` class provides 48 | a method to compute the beta parameter and one to compute the R squared coefficient 49 | specific to stocks in a portfolio when compared to the market index. 50 | 51 | """ 52 | 53 | # Attributes: 54 | investmentinfo: pd.DataFrame 55 | beta: Optional[FLOAT] 56 | rsquared: Optional[FLOAT] 57 | 58 | def __init__(self, investmentinfo: pd.DataFrame, data: pd.Series) -> None: 59 | """ 60 | :param investmentinfo: Investment information of a stock. 61 | :param data: Historical price data of a stock. 62 | """ 63 | self.name = investmentinfo.Name 64 | self.investmentinfo = investmentinfo 65 | super().__init__(data, self.name, asset_type="Stock") 66 | # beta parameter of stock (CAPM) 67 | self.beta = None 68 | # R squared coefficient of stock 69 | self.rsquared = None 70 | 71 | def comp_beta(self, market_daily_returns: pd.Series) -> FLOAT: 72 | """Computes and returns the Beta parameter of the stock. 73 | 74 | :param market_daily_returns: Daily returns of the market index. 75 | 76 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 77 | :return: Beta parameter of the stock 78 | """ 79 | # Type validations: 80 | type_validation(market_daily_returns=market_daily_returns) 81 | cov_mat = np.cov( 82 | self.comp_daily_returns(), 83 | market_daily_returns.to_frame()[market_daily_returns.name], 84 | ) 85 | 86 | beta = float(cov_mat[0, 1] / cov_mat[1, 1]) 87 | self.beta = beta 88 | return beta 89 | 90 | def comp_rsquared(self, market_daily_returns: pd.Series) -> FLOAT: 91 | """Computes and returns the R squared coefficient of the stock. 92 | 93 | :param market_daily_returns: Daily returns of the market index. 94 | 95 | :rtype: :py:data:`~.finquant.data_types.FLOAT` 96 | :return: R squared coefficient of the stock 97 | """ 98 | # Type validations: 99 | type_validation(market_daily_returns=market_daily_returns) 100 | 101 | rsquared = float( 102 | r2_score( 103 | market_daily_returns.to_frame()[market_daily_returns.name], 104 | self.comp_daily_returns(), 105 | ) 106 | ) 107 | self.rsquared = rsquared 108 | return rsquared 109 | 110 | def properties(self) -> None: 111 | """Nicely prints out the properties of the stock: Expected Return, 112 | Volatility, Beta (optional), R squared (optional), Skewness, Kurtosis as well as the ``Allocation`` 113 | (and other information provided in investmentinfo.) 114 | """ 115 | # nicely printing out information and quantities of the stock 116 | string = "-" * 50 117 | string += f"\n{self.asset_type}: {self.name}" 118 | string += f"\nExpected Return: {self.expected_return:0.3f}" 119 | string += f"\nVolatility: {self.volatility:0.3f}" 120 | if self.beta is not None: 121 | string += f"\n{self.asset_type} Beta: {self.beta:0.3f}" 122 | if self.rsquared is not None: 123 | string += f"\n{self.asset_type} R squared: {self.rsquared:0.3f}" 124 | string += f"\nSkewness: {self.skew:0.5f}" 125 | string += f"\nKurtosis: {self.kurtosis:0.5f}" 126 | string += "\nInformation:" 127 | string += "\n" + str(self.investmentinfo.to_frame().transpose()) 128 | string += "\n" + "-" * 50 129 | print(string) 130 | -------------------------------------------------------------------------------- /finquant/type_utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | ``finquant.type_utilities`` Module 4 | 5 | This module defines a type validation utility for working with various data types in Python, utilizing the 'numpy' 6 | and 'pandas' libraries. 7 | 8 | Dependencies: 9 | ------------- 10 | This module requires the following external libraries: 11 | 12 | - 'numpy' (imported as 'np') 13 | - 'pandas' (imported as 'pd') 14 | 15 | Example usage: 16 | -------------- 17 | Example: 18 | 19 | .. code-block:: python 20 | 21 | type_validation( 22 | data=pd.DataFrame([1., 2.]), 23 | names=["name1", "name2"], 24 | start_date="2023-08-01", 25 | freq=10, 26 | ) 27 | 28 | """ 29 | 30 | import datetime 31 | from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union 32 | 33 | import numpy as np 34 | import pandas as pd 35 | 36 | # supress some pylint complaints for this module only 37 | # pylint: disable=C0302,R0904,,R0912,W0212 38 | 39 | 40 | def _check_type( 41 | arg_name: str, 42 | arg_values: Any, 43 | expected_type: Union[Type[Any], Tuple[Type[Any], ...]], 44 | element_type: Optional[Union[Type[Any], Tuple[Type[Any], ...]]] = None, 45 | ) -> None: 46 | if isinstance(expected_type, tuple): 47 | class_names = [cls.__name__ for cls in expected_type] 48 | expected_type_string = ", ".join(class_names) 49 | else: 50 | expected_type_string = expected_type.__name__ 51 | 52 | element_type_string = None 53 | if element_type is not None: 54 | if isinstance(element_type, tuple): 55 | class_names = [cls.__name__ for cls in element_type] 56 | element_type_string = ", ".join(class_names) 57 | else: 58 | element_type_string = element_type.__name__ 59 | 60 | validation_failed = False 61 | 62 | if not isinstance(arg_values, expected_type): 63 | validation_failed = True 64 | 65 | if element_type is not None: 66 | if isinstance(arg_values, pd.DataFrame) and not all( 67 | arg_values.dtypes == element_type 68 | ): 69 | validation_failed = True 70 | 71 | if isinstance(arg_values, np.ndarray): 72 | if arg_values.ndim == 2 and not arg_values.dtype == element_type: 73 | validation_failed = True 74 | elif arg_values.ndim == 1 and not all( 75 | isinstance(val, element_type) for val in arg_values 76 | ): 77 | validation_failed = True 78 | 79 | elif isinstance(arg_values, List) and not all( 80 | isinstance(val, element_type) for val in arg_values 81 | ): 82 | validation_failed = True 83 | 84 | if validation_failed: 85 | error_msg = f"Error: {arg_name} is expected to be {expected_type_string}" 86 | if element_type_string: 87 | error_msg += f" with dtype '{element_type_string}'" 88 | raise TypeError(error_msg) 89 | 90 | 91 | def _check_callable_type( 92 | arg_name: str, 93 | arg_values: Any, 94 | ) -> None: 95 | if not callable(arg_values): 96 | error_msg = f"Error: {arg_name} is expected to be Callable" 97 | raise TypeError(error_msg) 98 | 99 | 100 | def _check_empty_data(arg_name: str, arg_values: Any) -> None: 101 | if isinstance(arg_values, (List, np.ndarray, pd.Series, pd.DataFrame)): 102 | if len(arg_values) == 0: 103 | raise ValueError( 104 | f"Error: {arg_name} is an empty list, numpy array, pandas series, or dataframe" 105 | ) 106 | 107 | 108 | # Define a dictionary mapping each argument name to its expected type and, if applicable, element type 109 | type_dict: Dict[ 110 | str, 111 | Tuple[ 112 | Union[Type[Any], Tuple[Type[Any], ...]], 113 | Optional[Union[Type[Any], Tuple[Type[Any], ...], None]], 114 | ], 115 | ] = { 116 | # DataFrames, Series, Array: 117 | "data": ((pd.Series, pd.DataFrame), np.floating), 118 | "pf_allocation": (pd.DataFrame, None), 119 | "returns_df": (pd.DataFrame, np.floating), 120 | "returns_series": (pd.Series, np.floating), 121 | "market_daily_returns": (pd.Series, np.floating), 122 | "means": ((np.ndarray, pd.Series), np.floating), 123 | "weights": ((np.ndarray, pd.Series), np.floating), 124 | "initial_weights": (np.ndarray, np.floating), 125 | "weights_array": (np.ndarray, np.floating), 126 | "cov_matrix": ((np.ndarray, pd.DataFrame), np.floating), 127 | # Lists: 128 | "names": ((List, np.ndarray), str), 129 | "cols": ((List, np.ndarray), str), 130 | "spans": ((List, np.ndarray), (int, np.integer)), 131 | "targets": ((List, np.ndarray), (int, np.integer)), 132 | # Datetime objects: 133 | "start_date": ((str, datetime.datetime), None), 134 | "end_date": ((str, datetime.datetime), None), 135 | # Strings: 136 | "data_api": (str, None), 137 | "market_index": (str, None), 138 | "method": (str, None), 139 | "name": (str, None), 140 | # FLOATs 141 | "expected_return": ((float, np.floating), None), 142 | "volatility": ((float, np.floating), None), 143 | "risk_free_rate": ((float, np.floating), None), 144 | "downside_risk": ((float, np.floating), None), 145 | "mu": ((float, np.floating), None), 146 | "sigma": ((float, np.floating), None), 147 | "conf_level": ((float, np.floating), None), 148 | "beta_parameter": ((float, np.floating), None), 149 | # INTs: 150 | "freq": ((int, np.integer), None), 151 | "span": ((int, np.integer), None), 152 | "num_trials": ((int, np.integer), None), 153 | # NUMERICs: 154 | "investment": ((int, np.integer, float, np.floating), None), 155 | "dividend": ((int, np.integer, float, np.floating), None), 156 | "target": ((int, np.integer, float, np.floating), None), 157 | # Booleans: 158 | "plot": (bool, None), 159 | "save_weights": (bool, None), 160 | "verbose": (bool, None), 161 | "defer_update": (bool, None), 162 | } 163 | 164 | type_callable_dict: Dict[ 165 | str, 166 | Tuple[ 167 | Callable[..., Any], 168 | Optional[Type[Any]], 169 | ], 170 | ] = { 171 | # Callables: 172 | "fun": (callable, None), 173 | } 174 | 175 | 176 | def type_validation(**kwargs: Any) -> None: 177 | """ 178 | Perform generic type validations on input variables. 179 | 180 | This function performs various type validations on a set of input variables. It helps to ensure that the input 181 | values conform to the expected types and conditions, raising a TypeError with a descriptive error message 182 | if any type validation fails and a ValueError if a numpy.array or pd.Series/DataFrame is empty. 183 | 184 | :param kwargs: Arbitrary keyword arguments representing the input variables to be checked. 185 | 186 | Raises: 187 | ``TypeError``: 188 | If any of the type validations fail, a TypeError is raised with a descriptive error message 189 | indicating the expected type and conditions for each variable. 190 | ``ValueError``: 191 | If any of the value validations fail, a ValueError is raised with a descriptive error message 192 | indicating the expected conditions for each variable. 193 | 194 | Example usage: 195 | 196 | .. code-block:: python 197 | 198 | type_validation( 199 | data=pd.DataFrame([1., 2.]), 200 | names=["name1", "name2"], 201 | start_date="2023-08-01", 202 | freq=10, 203 | ) 204 | """ 205 | 206 | for arg_name, arg_values in kwargs.items(): 207 | if arg_name not in type_dict and arg_name not in type_callable_dict: 208 | raise ValueError( 209 | f"Error: '{arg_name}' is not a valid argument. " 210 | f"Please only use argument names defined in `type_dict` or `type_callable_dict`." 211 | ) 212 | 213 | # Some arguments are allowed to be None, so skip them 214 | if arg_values is None: 215 | continue 216 | 217 | # Perform the type validation 218 | if arg_name == "fun": 219 | _check_callable_type(arg_name, arg_values) 220 | else: 221 | expected_type, element_type = type_dict[arg_name] 222 | # Validation of type 223 | _check_type(arg_name, arg_values, expected_type, element_type) 224 | # Check for empty list/array/series/dataframe 225 | _check_empty_data(arg_name, arg_values) 226 | -------------------------------------------------------------------------------- /images/finquant-logo-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmilthaler/FinQuant/55112a9b3d5182cb291225ea530fce5b403e49e6/images/finquant-logo-bw.png -------------------------------------------------------------------------------- /images/finquant-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmilthaler/FinQuant/55112a9b3d5182cb291225ea530fce5b403e49e6/images/finquant-logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | 4 | [tool.isort] 5 | profile = "black" 6 | 7 | [tool.mypy] 8 | python_version = "3.10" 9 | exclude = ["tests", "example"] 10 | strict = true 11 | strict_optional = true 12 | warn_return_any = true 13 | warn_no_return = true 14 | disallow_untyped_defs = true 15 | show_error_context = true 16 | ignore_missing_imports = true 17 | warn_unused_configs = true 18 | warn_unused_ignores = true 19 | plugins=["pydantic.mypy","numpy.typing.mypy_plugin"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.22.0 2 | scipy>=1.2.0 3 | pandas>=2.0 4 | matplotlib>=3.0 5 | quandl>=3.4.5 6 | yfinance>=0.1.43 7 | scikit-learn>=1.3.0 -------------------------------------------------------------------------------- /requirements_cd.txt: -------------------------------------------------------------------------------- 1 | build 2 | setuptools 3 | wheel 4 | twine -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | mypy 3 | isort 4 | jupyter 5 | notebook -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx==6.2.1 2 | sphinx_rtd_theme==1.2.0 3 | sphinx-autodoc-typehints -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.3.2 2 | black 3 | mypy 4 | isort 5 | pylint 6 | pydantic -------------------------------------------------------------------------------- /scripts/auto_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running auto commit" 4 | 5 | COMMITMSG=$1 6 | 7 | if [ -z "$COMMITMSG" ]; then 8 | COMMITMSG="Automated formatting changes" 9 | fi 10 | 11 | # Stage changes 12 | git add --update 13 | 14 | # Check Git diff-index 15 | git diff-index --quiet HEAD -- 16 | 17 | if [ $? -eq 0 ]; then 18 | echo "No changes found, nothing to see/do here." 19 | exit 1 20 | else 21 | echo "Changes found. Preparing commit." 22 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 23 | git config --local user.name "github-actions[bot]" 24 | git commit -m "${COMMITMSG}" 25 | fi 26 | -------------------------------------------------------------------------------- /scripts/auto_format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Code formatting with isort and black 4 | echo "Code formatting with isort and black:" 5 | isort $(git ls-files '*.py') 6 | black $(git ls-files '*.py') 7 | -------------------------------------------------------------------------------- /scripts/run_code_analysis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running Pylint - finquant (ignoring TODOs)" 4 | python -m pylint --fail-under=10 --disable=fixme --output-format=parseable *.py finquant | tee pylint.log 5 | #echo "Running Pylint - tests (ignoring TODOs and access to protected attributes)" 6 | #python -m pylint --disable=fixme,protected-access --output-format=parseable tests | tee -a pylint.log 7 | 8 | echo "" 9 | echo "Running Mypy" 10 | python -m mypy *.py finquant | tee mypy.log 11 | 12 | #echo "" 13 | #echo "Running Black (check mode only)" 14 | #python -m black --check *.py finquant tests 15 | 16 | #echo "" 17 | #echo "Running isort (check mode only)" 18 | #python -m isort --check *.py finquant tests -------------------------------------------------------------------------------- /scripts/update_readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | update_release_readme() { 4 | local version_file="version" 5 | local readme_md="$1" 6 | 7 | # Read the current release from the "version" file 8 | local current_release=$(grep -Eo 'release=([0-9]+\.){2}[0-9]+' "$version_file" | cut -d'=' -f2) 9 | 10 | update_file() { 11 | local file=$1 12 | sed -i "s/pypi-v[0-9]\+\.[0-9]\+\.[0-9]\+/pypi-v$current_release/" "$file" 13 | echo "Release updated to $current_release in $file" 14 | } 15 | 16 | # Update version in README.md 17 | update_file "$readme_md" 18 | } 19 | 20 | update_readme_tex() { 21 | local file_path="$1" 22 | 23 | # Copy README.md to README.tex.md 24 | cp README.md "$file_path" 25 | 26 | # Read the contents of README.tex.md 27 | local content=$(<"$file_path") 28 | 29 | # Replace patterns 30 | content=$(echo "$content" | sed -E "s//\$\\\\displaystyle\\\\dfrac{\\\\text{price}_{t_i} - \\\\text{price}_{t_0} + \\\\text{dividend}}{\\\\text{price}_{t_0}}\$/") 31 | 32 | content=$(echo "$content" | sed -E "s//\$\\\\displaystyle\\\\dfrac{\\\\text{price}_{t_i} - \\\\text{price}_{t_{i-1}}}{\\\\text{price}_{t_{i-1}}}\$/") 33 | 34 | content=$(echo "$content" | sed -E "s//\$\\\\displaystyle\\\\log\\\\left(1 + \\\\dfrac{\\\\text{price}_{t_i} - \\\\text{price}_{t_{i-1}}}{\\\\text{price}_{t_{i-1}}}\\\\right)\$/") 35 | 36 | # Write the updated contents back to README.tex.md 37 | echo "$content" > "$file_path" 38 | } 39 | 40 | # Update both readme files: 41 | echo "Updating README files:" 42 | update_release_readme "README.md" 43 | update_readme_tex "README.tex.md" 44 | -------------------------------------------------------------------------------- /scripts/update_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Version Management Module 3 | 4 | This module provides functions for managing version numbers based on branch names. 5 | 6 | Functions: 7 | ---------- 8 | - `increment_version(version: str, branch_name: str) -> str`: Increment the version based on the branch name pattern. 9 | - `increment_version_by(version: str, increment: str) -> str`: Increment the version by a given increment. 10 | - `read_version_from_file(filename: str) -> Optional[str]`: Read the version from a file. 11 | - `checkout_branch(branch_name: str) -> None`: Checkout a specific branch. 12 | - `get_version_from_branch(filename: str, branch_name: str) -> Optional[str]`: Get version number from a specific 13 | branch. 14 | - `compare_versions(version1: str, version2: str) -> int`: Compare two strings of version numbers. 15 | - `write_version_to_file(filename: str, version: str) -> None`: Write the updated version back to the file. 16 | - `parse_args() -> argparse.Namespace`: Parse command-line arguments. 17 | - `main() -> None`: Main function that handles version updates based on branch names. 18 | 19 | Exceptions: 20 | ----------- 21 | - `VersionFileReadError`: Exception raised when there is an error reading a version file. 22 | - `VersionUpdateError`: Exception raised when an error occurs during the update of a version. 23 | 24 | """ 25 | 26 | import argparse 27 | import re 28 | import subprocess 29 | import sys 30 | from typing import Optional, Tuple 31 | 32 | # Define the version increments based on the change type (patch, minor, major) 33 | version_increments = { 34 | "patch": "0.0.1", 35 | "minor": "0.1.0", 36 | "major": "1.0.0", 37 | } 38 | 39 | # Define the branch name prefixes for each change type 40 | branch_prefixes = { 41 | "patch": ["chore/", "refactor/", "bugfix/"], 42 | "minor": ["feature/"], 43 | "major": None, 44 | } 45 | 46 | 47 | class VersionFileReadError(Exception): 48 | """ 49 | Exception raised when there is an error reading a version file. 50 | """ 51 | 52 | 53 | class VersionUpdateError(Exception): 54 | """ 55 | Exception raised when an error occurs during the update of a version. 56 | """ 57 | 58 | 59 | # Function to increment the version based on the branch name pattern 60 | def increment_version(version: str, branch_name: str) -> str: 61 | """ 62 | Increment the version number based on the branch name pattern. 63 | 64 | Parameters: 65 | ----------- 66 | version (str): The current version number in "x.y.z" format. 67 | branch_name (str): The name of the branch being checked out. 68 | 69 | Returns: 70 | -------- 71 | str: The updated version number after applying the increment. 72 | 73 | """ 74 | 75 | for change_type, prefixes in branch_prefixes.items(): 76 | prefixes = prefixes or [] # If None, set to an empty list 77 | for prefix in prefixes: 78 | if branch_name.startswith(prefix): 79 | increment = version_increments[change_type] 80 | return ( 81 | increment_version_by(version, increment) if increment else version 82 | ) 83 | return version 84 | 85 | 86 | # Function to increment the version by a given increment (e.g., "0.0.1" or "0.1.0" or "1.0.0") 87 | def increment_version_by(version: str, increment: str) -> str: 88 | """ 89 | Increment the version by a given increment (e.g., "0.0.1" or "0.1.0" or "1.0.0"). 90 | 91 | Parameters: 92 | ----------- 93 | version (str): The current version number in "x.y.z" format. 94 | increment (str): The version increment to apply in "x.y.z" format. 95 | 96 | Returns: 97 | -------- 98 | str: The updated version number after applying the increment. 99 | 100 | """ 101 | 102 | version_parts = version.split(".") 103 | increment_parts = increment.split(".") 104 | 105 | new_version_parts = [] 106 | for idx, part in enumerate(version_parts): 107 | if idx < len(increment_parts): 108 | new_version_parts.append(str(int(part) + int(increment_parts[idx]))) 109 | else: 110 | new_version_parts.append("0") 111 | 112 | # If increment is "0.1.0", reset the third digit to 0 113 | if increment == "0.1.0" and len(version_parts) > 2: 114 | new_version_parts[2] = "0" 115 | 116 | # If increment is "1.0.0", reset the second and third digit to 0 117 | if increment == "1.0.0" and len(version_parts) > 2: 118 | new_version_parts[1] = "0" 119 | new_version_parts[2] = "0" 120 | 121 | return ".".join(new_version_parts) 122 | 123 | 124 | # Read the version from the file 125 | def read_version_from_file(filename: str) -> Optional[str]: 126 | """ 127 | Read the version from a file. 128 | 129 | Parameters: 130 | ----------- 131 | filename (str): The path to the file containing the version. 132 | 133 | Returns: 134 | -------- 135 | Optional[str]: The version number read from the file, or None if not found. 136 | 137 | """ 138 | 139 | with open(filename, "r") as file: 140 | version_content = file.read() 141 | version_match = re.search(r"version=(\d+\.\d+\.\d+)", version_content) 142 | if version_match: 143 | version = version_match.group(1) 144 | else: 145 | version = None 146 | return version 147 | 148 | 149 | # Function to checkout a specific branch 150 | def checkout_branch(branch_name: str) -> None: 151 | """ 152 | Checkout a specific branch to access its content. 153 | 154 | Parameters: 155 | ----------- 156 | branch_name (str): The name of the branch to be checked out. 157 | 158 | Returns: 159 | -------- 160 | None 161 | 162 | """ 163 | 164 | # Fetch the latest changes from the remote repository 165 | subprocess.run(["git", "fetch", "origin", branch_name], check=True) 166 | 167 | # Checkout the branch to access its content 168 | subprocess.run(["git", "checkout", branch_name], check=True) 169 | 170 | 171 | # Function to get version number from a specific branch 172 | def get_version_from_branch(filename: str, branch_name: str) -> Optional[str]: 173 | """ 174 | Get the version number from a specific branch. 175 | 176 | Parameters: 177 | ----------- 178 | filename (str): The path to the file containing the version. 179 | branch_name (str): The name of the branch from which to read the version. 180 | 181 | Returns: 182 | -------- 183 | Optional[str]: The version number read from the file, or None if not found. 184 | 185 | """ 186 | 187 | # Checkout branch 188 | checkout_branch(branch_name) 189 | 190 | # Get version from version file 191 | version = read_version_from_file(filename) 192 | 193 | # Read the version from the file 194 | return version 195 | 196 | 197 | # Function to compare 2 strings of version numbers 198 | def compare_versions(version1: str, version2: str) -> int: 199 | """ 200 | Compare two strings of version numbers. 201 | 202 | Parameters: 203 | ----------- 204 | version1 (str): The first version number to compare in "x.y.z" format. 205 | version2 (str): The second version number to compare in "x.y.z" format. 206 | 207 | Returns: 208 | -------- 209 | int: -1 if version1 < version2, 1 if version1 > version2, 0 if they are equal. 210 | 211 | """ 212 | 213 | def parse_version(version_str: str) -> Tuple[int, ...]: 214 | return tuple(map(int, version_str.split("."))) 215 | 216 | parsed_version1 = parse_version(version1) 217 | parsed_version2 = parse_version(version2) 218 | 219 | if parsed_version1 < parsed_version2: 220 | return -1 221 | elif parsed_version1 > parsed_version2: 222 | return 1 223 | else: 224 | return 0 225 | 226 | 227 | # Write the updated version back to the file 228 | def write_version_to_file(filename: str, version: str) -> None: 229 | """ 230 | Write the updated version back to the file. 231 | 232 | Parameters: 233 | ----------- 234 | filename (str): The path to the file to be updated. 235 | version (str): The updated version number in "x.y.z" format. 236 | 237 | Returns: 238 | -------- 239 | None 240 | 241 | """ 242 | 243 | with open(filename, "r+") as file: 244 | file_content = file.read() 245 | updated_content = re.sub( 246 | r"version=\d+\.\d+\.\d+", f"version={version}", file_content 247 | ) 248 | # Always set the release number to match the updated version number 249 | updated_content = re.sub( 250 | r"release=\d+\.\d+\.\d+", f"release={version}", updated_content 251 | ) 252 | file.seek(0) 253 | file.write(updated_content) 254 | file.truncate() 255 | 256 | 257 | # Function to parse command-line arguments 258 | def parse_args() -> argparse.Namespace: 259 | """ 260 | Parse command-line arguments. 261 | 262 | Returns: 263 | -------- 264 | argparse.Namespace: An object containing the parsed arguments. 265 | 266 | """ 267 | 268 | parser = argparse.ArgumentParser(description="Update version based on branch name.") 269 | parser.add_argument("base_branch", help="Base branch name") 270 | parser.add_argument("source_branch", help="Source branch name") 271 | return parser.parse_args() 272 | 273 | 274 | # Main function 275 | def main() -> None: 276 | """ 277 | Main function that handles version updates based on branch names. 278 | 279 | Returns: 280 | -------- 281 | None 282 | 283 | """ 284 | 285 | args = parse_args() 286 | base_branch_name = args.base_branch 287 | source_branch_name = args.source_branch 288 | 289 | file_path = "version" 290 | 291 | if base_branch_name != "master": 292 | raise ValueError("Base branch name must be 'master'.") 293 | 294 | if source_branch_name is None: 295 | raise ValueError("Source branch name must not be empty/None.") 296 | 297 | # Get the version from the base branch 298 | current_version_base = get_version_from_branch(file_path, base_branch_name) 299 | # Get the version from source branch 300 | current_version_source = get_version_from_branch(file_path, source_branch_name) 301 | 302 | # Sanity check for version numbers of base and source branch 303 | if current_version_base is None or current_version_source is None: 304 | raise VersionFileReadError( 305 | f"Failed to read the version from {base_branch_name} or from branch." 306 | ) 307 | 308 | # Increment the version based on the branch name pattern 309 | updated_version = increment_version(current_version_base, source_branch_name) 310 | 311 | print(f"Base branch: {base_branch_name}") 312 | print(f"Source branch: {source_branch_name}") 313 | print(f"Current version (base): {current_version_base}") 314 | print(f"Current version (source): {current_version_source}") 315 | print(f"Updated version: {updated_version}") 316 | 317 | # Check if updated version is higher than version in base branch: 318 | version_comparison = compare_versions(updated_version, current_version_base) 319 | if version_comparison < 0: 320 | raise VersionUpdateError( 321 | "Error: Updated version is lower than version in base branch." 322 | ) 323 | if version_comparison == 0: 324 | print("Version does not increase.") 325 | # Exit with error code 1 326 | sys.exit(1) 327 | if version_comparison > 0: 328 | if updated_version == current_version_source: 329 | print("Version is already updated.") 330 | # Exit with error code 1 331 | sys.exit(1) 332 | else: 333 | write_version_to_file(file_path, updated_version) 334 | print("Version updated in the file 'version'.") 335 | 336 | 337 | if __name__ == "__main__": 338 | main() 339 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license-file = LICENSE.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import setuptools 4 | 5 | 6 | def read_file(file_path: str) -> str: 7 | with open(file_path, "r") as file: 8 | return file.read() 9 | 10 | 11 | # get version/release from file 12 | version = dict(line.rstrip().split("=") for line in read_file("version").splitlines()) 13 | 14 | # get long description from README 15 | long_description = read_file("README.md") 16 | 17 | 18 | # get dependencies 19 | def read_requirements(file_path: str) -> List[str]: 20 | return [line.strip() for line in read_file(file_path).splitlines() if line.strip()] 21 | 22 | 23 | install_requires = read_requirements("requirements.txt") 24 | 25 | extras_require = { 26 | "cd": read_requirements("requirements_cd.txt"), 27 | "dev": read_requirements("requirements_dev.txt"), 28 | "docs": read_requirements("requirements_docs.txt"), 29 | "test": read_requirements("requirements_test.txt"), 30 | } 31 | 32 | setuptools.setup( 33 | name="FinQuant", 34 | version=version["version"], 35 | author="Frank Milthaler", 36 | author_email="f.milthaler@gmail.com", 37 | description="A program for financial portfolio management, analysis and optimisation", 38 | long_description=long_description, 39 | long_description_content_type="text/markdown", 40 | url="https://github.com/fmilthaler/FinQuant", 41 | download_url=f"https://github.com/fmilthaler/FinQuant/archive/v{version['release']}.tar.gz", 42 | license="MIT", 43 | packages=setuptools.find_packages(), 44 | classifiers=[ 45 | "Development Status :: 4 - Beta", 46 | "Intended Audience :: Education", 47 | "Intended Audience :: Financial and Insurance Industry", 48 | "Intended Audience :: Other Audience", 49 | "Intended Audience :: Science/Research", 50 | "Programming Language :: Python :: 3 :: Only", 51 | "Programming Language :: Python :: 3.10", 52 | "Programming Language :: Python :: 3.11", 53 | "License :: OSI Approved :: MIT License", 54 | "Operating System :: OS Independent", 55 | ], 56 | keywords=[ 57 | "finance", 58 | "portfolio", 59 | "investment", 60 | "numerical", 61 | "optimisation", 62 | "monte carlo", 63 | "efficient frontier", 64 | "quantitative", 65 | "quant", 66 | ], 67 | python_requires=">=3.10", 68 | install_requires=install_requires, 69 | extras_require=extras_require, 70 | project_urls={"Documentation": "https://finquant.readthedocs.io"}, 71 | ) 72 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | test:rempltshow 4 | @pytest -v . 5 | 6 | rempltshow:plt_agg_backend 7 | @-sed -i 's/plt.show()//g' test_Example*.py 8 | 9 | plt_agg_backend:set_api_key_example 10 | @-sed -i '/^import matplotlib\.pyplot as plt$$/a \plt.switch_backend("Agg")' test_Example*.py 11 | 12 | set_api_key_example: 13 | @-sed -i '/^from finquant\.portfolio import build_portfolio$$/a \import os;import quandl;quandl\.ApiConfig\.api_key = os\.getenv("QUANDLAPIKEY")' test_Example-Build-Portfolio-from-web.py 14 | 15 | clean: 16 | @-rm -rf *.pyc test_Example*.py __pycache__ 17 | 18 | -------------------------------------------------------------------------------- /tests/test_efficient_frontier.py: -------------------------------------------------------------------------------- 1 | #################################################### 2 | # for now, the corresponding module in finquant is # 3 | # tested through portfolio # 4 | #################################################### 5 | -------------------------------------------------------------------------------- /tests/test_market.py: -------------------------------------------------------------------------------- 1 | ################### 2 | # tests for Market # 3 | ################### 4 | 5 | import numpy as np 6 | import pandas as pd 7 | import pytest 8 | import yfinance 9 | 10 | from finquant.market import Market 11 | from finquant.portfolio import build_portfolio 12 | 13 | d = { 14 | 0: {"Name": "GOOG", "Allocation": 20}, 15 | 1: {"Name": "AMZN", "Allocation": 10}, 16 | 2: {"Name": "MCD", "Allocation": 15}, 17 | 3: {"Name": "DIS", "Allocation": 18}, 18 | 4: {"Name": "TSLA", "Allocation": 48}, 19 | } 20 | 21 | 22 | pf_allocation = pd.DataFrame.from_dict(d, orient="index") 23 | 24 | names_yf = pf_allocation["Name"].values.tolist() 25 | 26 | # dates can be set as datetime or string, as shown below: 27 | start_date = "2018-01-01" 28 | end_date = "2023-01-01" 29 | 30 | 31 | def test_Market(): 32 | pf = build_portfolio( 33 | names=names_yf, 34 | pf_allocation=pf_allocation, 35 | start_date=start_date, 36 | end_date=end_date, 37 | data_api="yfinance", 38 | market_index="^GSPC", 39 | ) 40 | assert isinstance(pf.market_index, Market) 41 | assert pf.market_index.name == "^GSPC" 42 | assert pf.beta is not None 43 | assert pf.rsquared is not None 44 | assert pf.treynor is not None 45 | -------------------------------------------------------------------------------- /tests/test_moving_average.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import pandas as pd 4 | 5 | from finquant.moving_average import ( 6 | compute_ma, 7 | ema, 8 | ema_std, 9 | plot_bollinger_band, 10 | sma, 11 | sma_std, 12 | ) 13 | 14 | plt.switch_backend("Agg") 15 | 16 | 17 | def test_sma(): 18 | orig = np.array( 19 | [ 20 | [np.nan, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5], 21 | [np.nan, 0.5, 2.5, 6.5, 12.5, 20.5, 30.5, 42.5, 56.5, 72.5], 22 | ] 23 | ) 24 | dforig = pd.DataFrame({"0": orig[0], "1": orig[1]}).dropna() 25 | l1 = range(10) 26 | l2 = [i**2 for i in range(10)] 27 | df = pd.DataFrame({"0": l1, "1": l2}).astype(np.float64) 28 | res = sma(df, span=2).dropna() 29 | assert all((dforig == res).all()) 30 | 31 | 32 | def test_ema(): 33 | orig = np.array( 34 | [ 35 | [ 36 | np.nan, 37 | 0.6666666666666666, 38 | 1.5555555555555556, 39 | 2.5185185185185186, 40 | 3.506172839506173, 41 | 4.502057613168724, 42 | 5.500685871056241, 43 | 6.500228623685413, 44 | 7.5000762078951375, 45 | 8.500025402631714, 46 | ], 47 | [ 48 | np.nan, 49 | 0.6666666666666666, 50 | 2.888888888888889, 51 | 6.962962962962963, 52 | 12.987654320987653, 53 | 20.99588477366255, 54 | 30.998628257887518, 55 | 42.99954275262917, 56 | 56.99984758420972, 57 | 72.99994919473657, 58 | ], 59 | ] 60 | ) 61 | dforig = pd.DataFrame({"0": orig[0], "1": orig[1]}).dropna() 62 | l1 = range(10) 63 | l2 = [i**2 for i in range(10)] 64 | df = pd.DataFrame({"0": l1, "1": l2}).astype(np.float64) 65 | res = ema(df, span=2).dropna() 66 | assert all((abs(dforig - res) <= 1e-15).all()) 67 | 68 | 69 | def test_sma_std(): 70 | orig = np.array( 71 | [ 72 | [ 73 | np.nan, 74 | 0.7071067811865476, 75 | 0.7071067811865476, 76 | 0.7071067811865476, 77 | 0.7071067811865476, 78 | 0.7071067811865476, 79 | 0.7071067811865476, 80 | 0.7071067811865476, 81 | 0.7071067811865476, 82 | 0.7071067811865476, 83 | ], 84 | [ 85 | np.nan, 86 | 0.7071067811865476, 87 | 2.1213203435596424, 88 | 3.5355339059327378, 89 | 4.949747468305833, 90 | 6.363961030678928, 91 | 7.7781745930520225, 92 | 9.192388155425117, 93 | 10.606601717798213, 94 | 12.020815280171307, 95 | ], 96 | ] 97 | ) 98 | dforig = pd.DataFrame({"0": orig[0], "1": orig[1]}).dropna() 99 | l1 = range(10) 100 | l2 = [i**2 for i in range(10)] 101 | df = pd.DataFrame({"0": l1, "1": l2}).astype(np.float64) 102 | res = sma_std(df, span=2).dropna() 103 | assert all((abs(dforig - res) <= 1e-15).all()) 104 | 105 | 106 | def test_ema_std(): 107 | orig = np.array( 108 | [ 109 | [ 110 | np.nan, 111 | 0.7071067811865476, 112 | 0.9746794344808964, 113 | 1.1143420667632726, 114 | 1.1785687889316867, 115 | 1.20612962779329, 116 | 1.217443715603457, 117 | 1.2219416913579804, 118 | 1.2236866244000921, 119 | 1.2243507269461653, 120 | ], 121 | [ 122 | np.nan, 123 | 0.7071067811865476, 124 | 2.2693611435820435, 125 | 4.280032864205755, 126 | 6.511621880314852, 127 | 8.846731940915395, 128 | 11.231335395956103, 129 | 13.640730921938678, 130 | 16.063365414263, 131 | 18.493615652686387, 132 | ], 133 | ] 134 | ) 135 | dforig = pd.DataFrame({"0": orig[0], "1": orig[1]}).dropna() 136 | l1 = range(10) 137 | l2 = [i**2 for i in range(10)] 138 | df = pd.DataFrame({"0": l1, "1": l2}).astype(np.float64) 139 | res = ema_std(df, span=2).dropna() 140 | assert all((abs(dforig - res) <= 1e-15).all()) 141 | 142 | 143 | def test_compute_ma(): 144 | stock_orig = [ 145 | 100.0, 146 | 0.1531138587991997, 147 | 0.6937500710898674, 148 | -0.9998892390840102, 149 | -0.46790785174554383, 150 | 0.24992469198859263, 151 | 0.8371986752411684, 152 | 0.9996789142433975, 153 | ] 154 | ma10d_orig = [ 155 | 91.0, 156 | 0.12982588130881456, 157 | 0.6364686654113839, 158 | -0.9111588177100766, 159 | -0.45638926346295605, 160 | 0.22777211487458476, 161 | 0.7335679046265856, 162 | 0.9544686462652832, 163 | ] 164 | ma30d_orig = [ 165 | 71.0, 166 | 0.029843302068984976, 167 | 0.40788852852895285, 168 | -0.5654851211095089, 169 | -0.3735139378462722, 170 | 0.0648917102227224, 171 | 0.4075702001405792, 172 | 0.5942823972334838, 173 | ] 174 | index = ["count", "mean", "std", "min", "25%", "50%", "75%", "max"] 175 | dforig = pd.DataFrame( 176 | {"Stock": stock_orig, "10d": ma10d_orig, "30d": ma30d_orig}, index=index 177 | ) 178 | x = np.sin(np.linspace(1, 10, 100)) 179 | df = pd.DataFrame({"Stock": x}).astype(np.float64) 180 | ma = compute_ma(df, ema, spans=[10, 30], plot=False) 181 | assert all(abs((dforig - ma.describe()) <= 1e-15).all()) 182 | 183 | 184 | def test_plot_bollinger_band(): 185 | labels_orig = ["Bollinger Band", "Stock", "15d"] 186 | xlabel_orig = "Days" 187 | ylabel_orig = "Price" 188 | title_orig = ( 189 | "Bollinger Band of +/- 2$\\sigma$, Moving Average " "of sma over 15 days" 190 | ) 191 | x = np.sin(np.linspace(1, 10, 100)) 192 | df = pd.DataFrame({"Stock": x}, index=np.linspace(1, 10, 100)).astype(np.float64) 193 | df.index.name = "Days" 194 | plt.figure() 195 | plot_bollinger_band(df, sma, span=15) 196 | # get data from axis object 197 | ax = plt.gca() 198 | # ax.lines[0] is the data we passed to plot_bollinger_band 199 | # ax.lines[1] is the moving average (already tested) 200 | # not sure how to obtain the data of the BollingerBand from 201 | # the plot. 202 | # only checking if input data matches data of first line on plot, 203 | # as a measure of data appearing in the plot 204 | line1 = ax.lines[0] 205 | stock_plot = line1.get_xydata() 206 | labels_plot = ax.get_legend_handles_labels()[1] 207 | xlabel_plot = ax.get_xlabel() 208 | ylabel_plot = ax.get_ylabel() 209 | title_plot = ax.get_title() 210 | # tests 211 | assert (df["Stock"].index.values == stock_plot[:, 0]).all() 212 | assert (df["Stock"].values == stock_plot[:, 1]).all() 213 | assert labels_orig == labels_plot 214 | assert xlabel_orig == xlabel_plot 215 | assert ylabel_orig == ylabel_plot 216 | assert title_orig == title_plot 217 | -------------------------------------------------------------------------------- /tests/test_optimisation.py: -------------------------------------------------------------------------------- 1 | #################################################### 2 | # for now, the corresponding module in finquant is # 3 | # tested through portfolio # 4 | #################################################### 5 | -------------------------------------------------------------------------------- /tests/test_quants.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | 7 | from finquant.quants import ( 8 | annualised_portfolio_quantities, 9 | downside_risk, 10 | sharpe_ratio, 11 | sortino_ratio, 12 | treynor_ratio, 13 | value_at_risk, 14 | weighted_mean, 15 | weighted_std, 16 | ) 17 | 18 | 19 | def test_weighted_mean(): 20 | means = np.array([1.0]) 21 | weights = np.array([1.0]) 22 | assert weighted_mean(means, weights) == 1 23 | means = np.array(range(5)).astype(np.float64) 24 | weights = np.array(range(5, 10)).astype(np.float64) 25 | assert weighted_mean(means, weights) == 80 26 | 27 | 28 | def test_weighted_std(): 29 | x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) 30 | y = np.array([9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0]) 31 | Sigma = np.cov(x, y) 32 | weights = np.array([1.0, 1.0]) 33 | assert weighted_std(Sigma, weights) == 0.0 34 | weights = np.array([-3.0, 5.0]) 35 | assert weighted_std(Sigma, weights) ** 2 == 480.0 36 | 37 | 38 | def test_sharpe_ratio(): 39 | assert sharpe_ratio(0.5, 0.2, 0.02) == 2.4 40 | assert sharpe_ratio(0.5, 0.22, 0.005) == 2.25 41 | 42 | 43 | def test_sortino_ratio(): 44 | assert sortino_ratio(0.5, 0.0, 0.02) is np.NaN 45 | assert sortino_ratio(0.005, 8.5, 0.005) == 0.0 46 | 47 | 48 | def test_treynor_ratio(): 49 | assert treynor_ratio(0.2, 0.9, 0.002) == 0.22 50 | assert treynor_ratio(0.005, 0.92, 0.005) == 0.0 51 | 52 | 53 | def test_value_at_risk(): 54 | assert abs(value_at_risk(1e2, 0.5, 0.25, 0.95) - 91.12) <= 1e-1 55 | assert abs(value_at_risk(1e3, 0.8, 0.5, 0.99) - 1963.17) <= 1e-1 56 | assert abs(value_at_risk(1e4, -0.1, 0.25, 0.9) - 2203.88) <= 1e-1 57 | assert abs(value_at_risk(1e4, 0.1, -0.25, 0.9) - (-2203.88)) <= 1e-1 58 | assert abs(value_at_risk(1e4, -0.1, -0.25, 0.9) - (-4203.88)) <= 1e-1 59 | assert value_at_risk(0, 0.1, 0.5, 0.9) == 0 60 | assert abs(value_at_risk(1e4, 0.0, 0.5, 0.9) - 6407.76) <= 1e-1 61 | assert abs(value_at_risk(1e4, 0.1, 0.0, 0.9) - 1000) <= 1e-1 62 | assert value_at_risk(1e4, 0.0, 0.0, 0.9) == 0 63 | 64 | 65 | def test_value_at_risk_invalid_types(): 66 | with pytest.raises(TypeError): 67 | value_at_risk("10000", 0.05, 0.02, 0.95) 68 | 69 | with pytest.raises(TypeError): 70 | value_at_risk(10000, 0.05, "0.02", 0.95) 71 | 72 | with pytest.raises(TypeError): 73 | value_at_risk(10000, [0.05], 0.02, 0.95) 74 | 75 | with pytest.raises(TypeError): 76 | value_at_risk(10000, 0.05, 0.02, "0.95") 77 | 78 | with pytest.raises(ValueError): 79 | value_at_risk(10000, 0.05, 0.02, 1.5) 80 | 81 | with pytest.raises(ValueError): 82 | value_at_risk(10000, 0.05, 0.02, -0.5) 83 | 84 | 85 | def test_annualised_portfolio_quantities(): 86 | x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) 87 | y = np.array([9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0]) 88 | Sigma = np.cov(x, y) 89 | mean = np.array([1.0, 2.0]) 90 | weights = np.array([-3.0, 5.0]) 91 | res = annualised_portfolio_quantities(weights, mean, Sigma, 0.0, 252) 92 | orig = (1764.0, 347.79304190854657, 5.071981861166303) 93 | for i in range(len(res)): 94 | assert abs(res[i] - orig[i]) <= 1e-15 95 | 96 | 97 | def test_downside_risk(): 98 | data1 = pd.DataFrame({"1": [1.0, 2.0, 4.0, 8.0], "2": [1.0, 2.0, 3.0, 4.0]}) 99 | weights = np.array([0.25, 0.75]) 100 | rf_rate = 0.005 101 | dr1 = downside_risk(data1, weights, rf_rate) 102 | assert dr1 == 0 103 | 104 | data2 = pd.DataFrame({"1": [7.0, 6.0, 5.0, 4.0, 3.0]}) 105 | weights = np.array([1.0]) 106 | rf_rate = 0.0 107 | dr2 = downside_risk(data2, weights, rf_rate) 108 | assert abs(dr2 - 0.19409143531019335) <= 1e-15 109 | -------------------------------------------------------------------------------- /tests/test_returns.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from finquant.returns import ( 5 | cumulative_returns, 6 | daily_log_returns, 7 | daily_returns, 8 | historical_mean_return, 9 | weighted_mean_daily_returns, 10 | ) 11 | 12 | 13 | def test_cumulative_returns(): 14 | orig = [ 15 | list(range(10)), 16 | [0.0, -0.025, -0.05, -0.075, -0.1, -0.125, -0.15, -0.175, -0.2, -0.225], 17 | ] 18 | l1 = np.array(range(1, 11)).astype(np.float64) 19 | l2 = [float(40 - i) for i in range(10)] 20 | d = {"1": l1, "2": l2} 21 | df = pd.DataFrame(d) 22 | ret = cumulative_returns(df) 23 | assert isinstance(ret, pd.DataFrame) and not ret.empty 24 | assert all(abs(ret["1"].values - orig[0]) <= 1e-15) 25 | assert all(abs(ret["2"].values - orig[1]) <= 1e-15) 26 | # with dividend of 0.2 27 | orig = [ 28 | [0.2 + i for i in range(10)], 29 | [0.005, -0.02, -0.045, -0.07, -0.095, -0.12, -0.145, -0.17, -0.195, -0.22], 30 | ] 31 | ret = cumulative_returns(df, 0.2) 32 | assert isinstance(ret, pd.DataFrame) and not ret.empty 33 | assert all(abs(ret["1"].values - orig[0]) <= 1e-15) 34 | assert all(abs(ret["2"].values - orig[1]) <= 1e-15) 35 | 36 | 37 | def test_daily_returns(): 38 | orig = [[1.0, 1.0 / 2, 1.0 / 3, 1.0 / 4], [1.0 / 9, 1.0 / 10, 1.0 / 11, 1.0 / 12]] 39 | l1 = np.array(range(1, 6)).astype(np.float64) 40 | l2 = [10 * 0.2 + i * 0.25 for i in range(1, 6)] 41 | d = {"1": l1, "2": l2} 42 | df = pd.DataFrame(d) 43 | ret = daily_returns(df) 44 | assert all(abs(ret["1"].values - orig[0]) <= 1e-15) 45 | assert all(abs(ret["2"].values - orig[1]) <= 1e-15) 46 | 47 | 48 | def test_weighted_daily_mean_returns(): 49 | l1 = [1.0, 1.5, 2.25, 3.375] 50 | l2 = [1.0, 2.0, 4.0, 8.0] 51 | expected = [0.5 * 0.25 + 1 * 0.75 for i in range(len(l1) - 1)] 52 | weights = np.array([0.25, 0.75]) 53 | d = {"1": l1, "2": l2} 54 | df = pd.DataFrame(d) 55 | ret = weighted_mean_daily_returns(df, weights) 56 | assert all(abs(ret - expected) <= 1e-15) 57 | 58 | d = {"1": l1} 59 | expected = [0.5 for i in range(len(l1) - 1)] 60 | df = pd.DataFrame(d) 61 | ret = weighted_mean_daily_returns(df, np.array([1.0])) 62 | assert all(abs(ret - expected) <= 1e-15) 63 | 64 | 65 | def test_daily_log_returns(): 66 | orig = [ 67 | [ 68 | 0.6931471805599453, 69 | 0.4054651081081644, 70 | 0.28768207245178085, 71 | 0.22314355131420976, 72 | ], 73 | [ 74 | 0.10536051565782635, 75 | 0.09531017980432493, 76 | 0.0870113769896297, 77 | 0.08004270767353636, 78 | ], 79 | ] 80 | l1 = np.array(range(1, 6)).astype(np.float64) 81 | l2 = [10 * 0.2 + i * 0.25 for i in range(1, 6)] 82 | d = {"1": l1, "2": l2} 83 | df = pd.DataFrame(d) 84 | ret = daily_log_returns(df) 85 | ret 86 | 87 | assert all(abs(ret["1"].values - orig[0]) <= 1e-15) 88 | assert all(abs(ret["2"].values - orig[1]) <= 1e-15) 89 | 90 | 91 | def test_historical_mean_return(): 92 | orig = [13.178779135809942, 3.8135072274034982] 93 | l1 = np.array(range(1, 101)).astype(np.float64) 94 | l2 = [10 * 0.2 + i * 0.25 for i in range(21, 121)] 95 | d = {"1": l1, "2": l2} 96 | df = pd.DataFrame(d) 97 | ret = historical_mean_return(df, freq=252) 98 | assert abs(ret["1"] - orig[0]) <= 1e-15 99 | assert abs(ret["2"] - orig[1]) <= 1e-15 100 | -------------------------------------------------------------------------------- /tests/test_stock.py: -------------------------------------------------------------------------------- 1 | ################### 2 | # tests for Stock # 3 | ################### 4 | 5 | import datetime 6 | import os 7 | import pathlib 8 | 9 | import numpy as np 10 | import pandas as pd 11 | import pytest 12 | import quandl 13 | import yfinance 14 | 15 | from finquant.portfolio import build_portfolio 16 | from finquant.stock import Stock 17 | 18 | # comparisons 19 | strong_abse = 1e-15 20 | weak_abse = 1e-8 21 | 22 | # setting quandl api key 23 | quandl.ApiConfig.api_key = os.getenv("QUANDLAPIKEY") 24 | 25 | # read data from file 26 | df_pf_path = pathlib.Path.cwd() / ".." / "data" / "ex1-portfolio.csv" 27 | df_data_path = pathlib.Path.cwd() / ".." / "data" / "ex1-stockdata.csv" 28 | df_pf = pd.read_csv(df_pf_path) 29 | df_data = pd.read_csv(df_data_path, index_col="Date", parse_dates=True) 30 | # create testing variables 31 | names = df_pf.Name.values.tolist() 32 | names_yf = [name.replace("WIKI/", "") for name in names] 33 | weights_df_pf = [ 34 | 0.31746031746031744, 35 | 0.15873015873015872, 36 | 0.23809523809523808, 37 | 0.2857142857142857, 38 | ] 39 | weights_no_df_pf = [1.0 / len(names) for i in range(len(names))] 40 | df_pf2 = pd.DataFrame({"Allocation": weights_no_df_pf, "Name": names}) 41 | df_pf2_yf = pd.DataFrame({"Allocation": weights_no_df_pf, "Name": names_yf}) 42 | start_date = datetime.datetime(2015, 1, 1) 43 | end_date = "2017-12-31" 44 | 45 | # create kwargs to be passed to build_portfolio 46 | d_pass = [ 47 | { 48 | "names": names, 49 | "start_date": start_date, 50 | "end_date": end_date, 51 | "data_api": "quandl", 52 | } 53 | ] 54 | 55 | 56 | def test_Stock(): 57 | d = d_pass[0] 58 | pf = build_portfolio(**d) 59 | # loop over all stocks stored within pf and check that values 60 | # are equal to the ones in pf 61 | for i in range(len(pf.stocks)): 62 | assert isinstance(pf.get_stock(names[0]), Stock) 63 | stock = pf.get_stock(names[i]) 64 | assert stock.name == pf.portfolio["Name"][i] 65 | assert all(stock.data - pf.data[stock.name].to_frame() <= strong_abse) 66 | assert all( 67 | stock.investmentinfo == pf.portfolio.loc[pf.portfolio["Name"] == stock.name] 68 | ) 69 | -------------------------------------------------------------------------------- /tex/27215e5f36fd0308b51ab510444edf0d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tex/738645698dc3073b4bb52a0c078ae829.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | version=0.7.0 2 | release=0.7.0 3 | --------------------------------------------------------------------------------