From 0b26d9f70b8200edf21d8fd78b35f4d7b0acd383 Mon Sep 17 00:00:00 2001 From: Sergey Kikevich Date: Wed, 15 Sep 2021 13:39:29 +0300 Subject: [PATCH] docs: change sphinx configuration Jupyter Notebooks from examples can be used in docs --- docs/_templates/custom-class-template.rst | 20 + docs/conf.py | 44 +- docs/index.rst | 33 +- docs/make.bat | 4 + docs/quickstart.nblink | 3 + docs/requirements.txt | 2 + docs/rst.bat | 3 + docs/source/asset_list.rst | 13 - docs/source/multi_period.rst | 19 - docs/source/plots.rst | 10 - docs/source/portfolio.rst | 12 - docs/source/single_period.rst | 15 - examples/01 howto.ipynb | 13 +- okama/asset.py | 26 +- okama/asset_list.py | 7 + okama/portfolio.py | 516 ++++++++++++++-------- 16 files changed, 427 insertions(+), 313 deletions(-) create mode 100644 docs/_templates/custom-class-template.rst create mode 100644 docs/quickstart.nblink delete mode 100644 docs/source/asset_list.rst delete mode 100644 docs/source/multi_period.rst delete mode 100644 docs/source/plots.rst delete mode 100644 docs/source/portfolio.rst delete mode 100644 docs/source/single_period.rst diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst new file mode 100644 index 0000000..b5f9d17 --- /dev/null +++ b/docs/_templates/custom-class-template.rst @@ -0,0 +1,20 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block members %} + {% if members %} + .. rubric:: {{ _('Methods & Attributes') }} + + .. autosummary:: + :toctree: + :caption: Methods & Attributes + {% for item in members %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 1dece43..c3ad061 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,5 @@ # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys @@ -44,14 +41,14 @@ # The full version, including alpha/beta/rc tags. release = version +# -- General configuration --------------------------------------------------- + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. language = "en" -# -- General configuration --------------------------------------------------- - -autodoc_default_flags = ["members"] -autosummary_generate = ["index"] +# The encoding of source files. +source_encoding = "utf-8" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -66,6 +63,7 @@ "sphinx.ext.mathjax", "sphinx_rtd_theme", "nbsphinx", + "nbsphinx_link", "recommonmark", # "myst_parser", # for markdown ] @@ -93,12 +91,14 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -# html_theme_options = { -# "external_links": [], -# # "github_url": "https://github.com/mbk-dev/okama", -# # "twitter_url": "https://twitter.com/pandas_dev", -# # "google_analytics_id": "UA-27880019-2", -# } +html_theme_options = { + # "external_links": [], + # "github_url": "https://github.com/mbk-dev/okama", + # "google_analytics_id": "UA-27880019-2", + # Toc options + "titles_only": False, + "navigation_depth": 4, +} # If false, no module index is generated. html_use_modindex = False @@ -110,11 +110,16 @@ # -- Options for autodoc ------------------------------------------------ -# This value controls how to represents typehints. +autodoc_default_flags = ["members"] +autodoc_default_options = { + 'undoc-members': False, + 'exclude-members': '__init__'} autodoc_typehints = "none" autodoc_member_order = "bysource" autoclass_content = "class" # to not insert __init__ docstrings autodoc_class_signature = "mixed" # Display the signature with the class name. +autosummary_generate = True +autosummary_imported_members = False # -- Options for numpydoc ------------------------------------------------ numpydoc_attributes_as_param_list = False @@ -124,16 +129,20 @@ # -- Options for nbsphinx ------------------------------------------------ +nbsphinx_execute_arguments = [ + "--InlineBackend.figure_formats={'svg', 'pdf'}", + "--InlineBackend.rc=figure.dpi=96", +] # nbsphinx do not use requirejs (breaks bootstrap) nbsphinx_requirejs_path = "" -# matplotlib plot directive settings +# -- matplotlib plot directive settings ----------------------------------- plot_html_show_formats = False plot_include_source = True plot_html_show_source_link = False +plot_formats = [("png", 90)] plot_pre_code = """ import numpy as np -from matplotlib import pyplot as plt import okama as ok """ @@ -151,6 +160,3 @@ # napoleon_use_rtype = True # napoleon_type_aliases = None # napoleon_attr_annotations = True - -# The encoding of source files. -source_encoding = "utf-8" diff --git a/docs/index.rst b/docs/index.rst index 659468e..12df0c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ python   - pypi   =4.1.2 sphinx_rtd_theme==0.5.2 numpydoc==1.1.0 nbsphinx==0.8.7 +nbsphinx-link>=1.3.0 +pandoc>=2.14.0 recommonmark==0.7.1 okama>=1.0.0 diff --git a/docs/rst.bat b/docs/rst.bat index 7bc0871..401f519 100644 --- a/docs/rst.bat +++ b/docs/rst.bat @@ -1 +1,4 @@ +@ECHO OFF +rem generate .rst sorce files + sphinx-apidoc -o source/ ../okama diff --git a/docs/source/asset_list.rst b/docs/source/asset_list.rst deleted file mode 100644 index 7ab4fe1..0000000 --- a/docs/source/asset_list.rst +++ /dev/null @@ -1,13 +0,0 @@ -========== -Asset List -========== - -In investing, it is often necessary to compare the performance of different types of assets, stock indices, -currencies, and portfolios. For the convenience of such comparisons, *okama* library has the **AssetList class**. - -AssetList includes several methods to analyze and compare historical return, various risk metrics of the assets (and indexes). - - -.. autoclass:: okama.AssetList - :members: - :inherited-members: diff --git a/docs/source/multi_period.rst b/docs/source/multi_period.rst deleted file mode 100644 index 114eca7..0000000 --- a/docs/source/multi_period.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _multi_period: - -========================= -Multi-Period Optimization -========================= - -In single period optimization portfolio is always rebalanced and has original weights. However, in real life portfolios -are not rebalanced every day or every moth. - -In multi-period approach portfolio is rebalanced to the original allocation with a certain frequency (annually, quarterly etc.) or not rebalanced at all. - -EfficientFrontierReb class can be used for multi-period optimization. Two rebalancing frequencies can be usd (reb_period parameter): - -- 'year' - one Year (default) -- 'none' - not rebalanced portfolios - - -.. autoclass:: okama.EfficientFrontierReb - :members: diff --git a/docs/source/plots.rst b/docs/source/plots.rst deleted file mode 100644 index 27d7403..0000000 --- a/docs/source/plots.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _plots: - -=============== -Financial Plots -=============== - -**Plots** helper class contains methods that make it easier to create frequently used financial visualizations. - -.. autoclass:: okama.Plots - :members: diff --git a/docs/source/portfolio.rst b/docs/source/portfolio.rst deleted file mode 100644 index 3c3500b..0000000 --- a/docs/source/portfolio.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _portfolio: - -========== -Portfolio -========== - -Investments portfolio is a type of financial asset. - - -.. autoclass:: okama.Portfolio - :members: - :inherited-members: diff --git a/docs/source/single_period.rst b/docs/source/single_period.rst deleted file mode 100644 index 24cae0d..0000000 --- a/docs/source/single_period.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. _single_period: - -============================== -Single Period MVA Optimization -============================== - -A **single period optimization** is used in classic Harry Markowitz Mean-Variance Analysis (MVA) where a portfolio is always -rebalanced and has original weights. -In *okama* it's equivalent to monthly rebalanced portfolios as monthly historical data is used. - -MVA Optimization is the most easy and fast way to draw an Efficient Frontier. - - -.. autoclass:: okama.EfficientFrontier - :members: diff --git a/examples/01 howto.ipynb b/examples/01 howto.ipynb index cca20fa..52d5f4a 100644 --- a/examples/01 howto.ipynb +++ b/examples/01 howto.ipynb @@ -125,6 +125,13 @@ "one_asset.dividends.tail(10)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fiancial Databse: Tickers & Namespaces" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -183,7 +190,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Namespaces" + "Namespase is a set of characters after the period in the ticker (SPY**.US**)." ] }, { @@ -424,7 +431,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Compare several assets from different stock markets" + "## Compare assets from different stock markets" ] }, { @@ -933,7 +940,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Correlation Matrix" + "### Correlation Matrix" ] }, { diff --git a/okama/asset.py b/okama/asset.py index 4113616..94e6db7 100644 --- a/okama/asset.py +++ b/okama/asset.py @@ -17,23 +17,6 @@ class Asset: ---------- symbol: str, default "SPY.US" Symbol is an asset ticker with namespace after dot. The default value is "SPY.US" (SPDR S&P 500 ETF Trust). - - Examples - -------- - >>> asset = ok.Asset() - >>> asset - symbol SPY.US - name SPDR S&P 500 ETF Trust - country USA - exchange NYSE ARCA - currency USD - type ETF - first date 1993-02 - last date 2021-03 - period length 28.1 - dtype: object - - An Asset object could be easy created whithout specifying a symbol Asset() using the default symbol. """ def __init__(self, symbol: str = default_ticker): @@ -124,10 +107,19 @@ def close_monthly(self): """ Return close price time series historical monthly data. + Monthly close time series not adjusted to for corporate actions: dividends and splits. + Returns ------- Series Time series of close price historical data (monthly). + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> x = ok.Asset('VOO.US') + >>> x.close_monthly.plot() + >>> plt.show() """ return Frame.change_period_to_month(self.close_daily) diff --git a/okama/asset_list.py b/okama/asset_list.py index bc7eec3..04f164f 100644 --- a/okama/asset_list.py +++ b/okama/asset_list.py @@ -64,6 +64,13 @@ def wealth_indexes(self) -> pd.DataFrame: ------- DataFrame Time series of wealth index values for each asset and accumulated inflation. + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> x = ok.AssetList(['SPY.US', 'BND.US']) + >>> x.wealth_indexes.plot() + >>> plt.show() """ df = self._add_inflation() return Frame.get_wealth_indexes(df) diff --git a/okama/portfolio.py b/okama/portfolio.py index ebd5a70..8c1c299 100644 --- a/okama/portfolio.py +++ b/okama/portfolio.py @@ -289,19 +289,10 @@ def wealth_index(self) -> pd.DataFrame: Examples -------- + >>> import matplotlib.pyplot as plt >>> x = ok.Portfolio(['SPY.US', 'BND.US']) - >>> x.wealth_index - portfolio USD.INFL - 2007-05 1000.000000 1000.000000 - 2007-06 1004.034950 1008.011590 - 2007-07 992.940364 1007.709187 - 2007-08 1006.642941 1005.895310 - ... ... - 2020-12 2561.882476 1260.242835 - 2021-01 2537.800781 1265.661880 - 2021-02 2553.408256 1272.623020 - 2021-03 2595.156481 1281.658643 - [167 rows x 2 columns] + >>> x.wealth_index.plot() + >>> plt.show() """ df = self._add_inflation() df = Frame.get_wealth_indexes(df) @@ -333,18 +324,10 @@ def wealth_index_with_assets(self) -> pd.DataFrame: Examples -------- + >>> import matplotlib.pyplot as plt >>> pf = ok.Portfolio(['VOO.US', 'GLD.US'], weights=[0.8, 0.2]) - >>> pf.wealth_index_with_assets - portfolio VOO.US GLD.US USD.INFL - 2010-10 1000.000000 1000.000000 1000.000000 1000.000000 - 2010-11 1041.065584 1036.658420 1058.676480 1001.600480 - 2010-12 1103.779375 1108.395183 1084.508186 1003.303201 - 2011-01 1109.298272 1133.001556 1015.316564 1008.119056 - ... ... ... ... - 2020-12 3381.729677 4043.276231 1394.513920 1192.576493 - 2021-01 3332.356424 4002.034813 1349.610572 1197.704572 - 2021-02 3364.480340 4112.891178 1265.124950 1204.291947 - 2021-03 3480.083884 4301.261594 1250.702526 1212.842420 + >>> pf.wealth_index_with_assets.plot() + >>> plt.show() """ if hasattr(self, "inflation"): df = pd.concat( @@ -415,22 +398,16 @@ def annual_return_ts(self) -> pd.Series: Examples -------- + >>> import matplotlib.pyplot as plt >>> pf = ok.Portfolio(['VOO.US', 'AGG.US'], weights=[0.4, 0.6]) - >>> pf.annual_return_ts - Date - 2010 0.034299 - 2011 0.056599 - 2012 0.086613 - 2013 0.107111 - 2014 0.090420 - 2015 0.010381 - 2016 0.063620 - 2017 0.105450 - 2018 -0.013262 - 2019 0.174182 - 2020 0.124668 - 2021 0.030430 - Freq: A-DEC, Name: portfolio_5364.PF, dtype: float64 + >>> pf.annual_return_ts.plot(kind='bar') + >>> plt.show() + + Plot annual returns for portfolio with EUR as the base currency. + + >>> pf = ok.Portfolio(['VOO.US', 'AGG.US'], weights=[0.4, 0.6], ccy='EUR') + >>> pf.annual_return_ts.plot(kind='bar') + >>> plt.show() """ return Frame.get_annual_return_ts_from_monthly(self.ror) @@ -469,6 +446,7 @@ def get_cagr(self, period: Optional[int] = None, real: bool = False) -> pd.Serie {'XCS6.XETR': 'Xtrackers MSCI China UCITS ETF 1C', 'PHAU.LSE': 'WisdomTree Physical Gold'} To get inflation adjusted return (real annualized return) add `real=True` option: + >>> pf.get_cagr(period=5, real=True) portfolio_5625.PF 0.121265 dtype: float64 @@ -1144,7 +1122,9 @@ def percentile_inverse_cagr( ---------- distr: {'norm', 'lognorm', 'hist'}, default 'norm' The rate of teturn distribution type. - For 'hist' type percentile is taken from the historical data. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + 'hist' - percentiles are taken from the historical data. years: int, default 1 Period length (time frame) in years when CAGR is calculated. @@ -1304,6 +1284,8 @@ def monte_carlo_returns_ts( ---------- distr : {'norm', 'lognorm'}, default 'norm' Distribution type for rate of return time series. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. years : int, default 1 Forecast period for portfolio monthly rate of return time series. @@ -1431,6 +1413,8 @@ def percentile_distribution_cagr( ---------- distr : {'norm', 'lognorm'}, default 'norm' Distribution type for the rate of return of portfolio. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. years: int, default 1 Time frame for portfolio CAGR. @@ -1487,7 +1471,9 @@ def percentile_wealth( ---------- distr : {'hist', 'norm', 'lognorm'}, default 'norm' Distribution type for the rate of return of portfolio. - For 'hist' type percentiles are taken from the historical data. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + 'hist' - percentiles are taken from the historical data. years : int, default 1 Investment period length to calculate wealth index. @@ -1540,138 +1526,11 @@ def percentile_wealth( results.update((x, y * modifier) for x, y in results.items()) return results - def plot_forecast( - self, - distr: str = "norm", - years: int = 5, - percentiles: List[int] = [10, 50, 90], - today_value: Optional[int] = None, - n: int = 1000, - figsize: Optional[tuple] = None, - ): - """ - Plot forecasted ranges of wealth indexes (lines) for a given set of percentiles. - Historical wealth index is shown in the same chart. - - Parameters - ---------- - distr : {'hist', 'norm', 'lognorm'}, default 'norm' - Distribution type for the rate of return of portfolio. - For 'hist' type percentiles are taken from the historical data. - - years : int, default 1 - Investment period length to calculate wealth index. - It should not exceed 1/2 of the portfolio history period length 'period_length'. - - percentiles : list of int, default [10, 50, 90] - List of percentiles to be calculated. - - today_value : int, optional - Initial value of the wealth index. - If today_value is None the last value of the historical wealth indexes is taken. It can be useful to plot - the forecast of wealth index togeather with the hitorical data. - - n : int, default 1000 - Number of random time series to generate with Monte Carlo simulation (for 'norm' or 'lognorm' only). - Larger argument values can be used to increase the precision of the calculation. But this will lead - to slower performance. - Is not required for historical distribution (dist='hist'). - - Returns - ------- - Axes : 'matplotlib.axes._subplots.AxesSubplot' - - Examples - -------- - >>> import matplotlib.pyplot as plt - >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='year') - >>> pf.plot_forecast() - >>> plt.show() - """ - wealth = self.wealth_index - x1 = self.last_date - x2 = x1.replace(year=x1.year + years) - y_start_value = wealth[self.symbol].iloc[-1] - y_end_values = self.percentile_wealth( - distr=distr, years=years, percentiles=percentiles, n=n - ) - if today_value: - modifier = today_value / y_start_value - wealth *= modifier - y_start_value = y_start_value * modifier - y_end_values.update((x, y * modifier) for x, y in y_end_values.items()) - fig, ax = plt.subplots(figsize=figsize) - ax.plot( - wealth.index.to_timestamp(), - wealth[self.symbol], - linewidth=1, - label="Historical data", - ) - for percentile in percentiles: - x, y = [x1, x2], [y_start_value, y_end_values[percentile]] - if percentile == 50: - ax.plot(x, y, color="blue", linestyle="-", linewidth=2, label="Median") - else: - ax.plot( - x, - y, - linestyle="dashed", - linewidth=1, - label=f"Percentile {percentile}", - ) - ax.legend(loc="upper left") - return ax - - def plot_forecast_monte_carlo( - self, - distr: str = "norm", - years: int = 1, - n: int = 20, - figsize: Optional[tuple] = None, - ): - """ - Plot Monte Carlo simulation for portfolio wealth indexes together with historical wealth index. - - Random wealth indexes are generated according to a given distribution. - - Parameters - ---------- - distr : {'norm', 'lognorm'}, default 'norm' - Distribution type for the rate of return of portfolio. - - years : int, default 1 - Investment period length for new wealth indexes - It should not exceed 1/2 of the portfolio history period length 'period_length'. - - n : int, default 20 - Number of random wealth indexes to generate with Monte Carlo simulation. - - figsize : (float, float), optional - Width, height in inches. - If None default matplotlib figsize value is used. - - Returns - ------- - Axes : 'matplotlib.axes._subplots.AxesSubplot' - - Examples - -------- - >>> import matplotlib.pyplot as plt - >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='year') - >>> pf.plot_forecast_monte_carlo(years=5, distr='lognorm', n=100) - >>> plt.show() - """ - s1 = self.wealth_index - s2 = self._monte_carlo_wealth(distr=distr, years=years, n=n) - s1[self.symbol].plot(legend=None, figsize=figsize) - for n in s2: - s2[n].plot(legend=None) - # distributions @property def skewness(self) -> pd.Series: """ - Compute expanding skewness of return time series. + Compute expanding skewness time series for portfolio rate of return. For normally distributed data, the skewness should be about zero. A skewness value greater than zero means that there is more weight in the right tail of the distribution. @@ -1696,6 +1555,10 @@ def skewness(self) -> pd.Series: 2021-06 0.437383 2021-07 0.425247 Freq: M, Name: portfolio_8378.PF, Length: 159, dtype: float64 + + >>> import matplotlib.pyplot as plt + >>> pf.skewness.plot() + >>> plt.show() """ return Frame.skewness(self.ror) @@ -1703,7 +1566,7 @@ def skewness_rolling(self, window: int = 60): """ Compute rolling skewness of the return time series. - For normally distributed data, the skewness should be about zero. + For normally distributed rate of return, the skewness should be about zero. A skewness value greater than zero means that there is more weight in the right tail of the distribution. Parameters @@ -1736,58 +1599,188 @@ def skewness_rolling(self, window: int = 60): 2021-07 -0.012192 Freq: M, Name: portfolio_8378.PF, dtype: float64 + >>> import matplotlib.pyplot as plt + >>> pf.skewness_rolling(window=12*10).plot() + >>> plt.show() """ return Frame.skewness_rolling(self.ror, window=window) @property def kurtosis(self): """ - Calculate expanding Fisher (normalized) kurtosis time series for portfolio returns. + Calculate expanding Fisher (normalized) kurtosis time series for portfolio rate of return. + + Kurtosis is a measure of whether the rate of return are heavy-tailed or light-tailed + relative to a normal distribution. + It should be close to zero for normally distributed rate of return. Kurtosis is the fourth central moment divided by the square of the variance. - Kurtosis should be close to zero for normal distribution. + + Returns + ------- + Series + Expanding kurtosis time series + + Examples + -------- + >>> pf = ok.Portfolio(['BND.US']) + >>> pf.kurtosis + Date + 2008-05 -0.815206 + 2008-06 -0.718330 + 2008-07 -0.610741 + 2008-08 -0.534105 + ... + 2021-04 2.821322 + 2021-05 2.855267 + 2021-06 2.864717 + 2021-07 2.850407 + Freq: M, Name: portfolio_4411.PF, Length: 159, dtype: float64 + + >>> import matplotlib.pyplot as plt + >>> pf.kurtosis.plot() + >>> plt.show() """ return Frame.kurtosis(self.ror) def kurtosis_rolling(self, window: int = 60): """ - Calculate rolling Fisher (normalized) kurtosis time series for portfolio returns. + Calculate rolling Fisher (normalized) kurtosis time series for portfolio rate of return. + + Kurtosis is a measure of whether the rate of return are heavy-tailed or light-tailed + relative to a normal distribution. + It should be close to zero for normally distributed rate of return. Kurtosis is the fourth central moment divided by the square of the variance. - Kurtosis should be close to zero for normal distribution. - window - the rolling window size in months (default is 5 years). - The window size should be at least 12 months. + Parameters + ---------- + window : int, default 60 + Size of the moving window in months. + The window size should be at least 12 months. + + Returns + ------- + Series + Expanding kurtosis time series. + + Examples + -------- + >>> pf = ok.Portfolio(['BND.US']) + >>> pf.kurtosis_rolling(window=12*10) + Date + 2017-04 4.041599 + 2017-05 4.133518 + 2017-06 4.165099 + 2017-07 4.205125 + 2017-08 4.313773 + ... + 2021-03 0.362184 + 2021-04 0.409680 + 2021-05 0.455760 + 2021-06 0.457315 + 2021-07 0.496168 + Freq: M, Name: portfolio_4411.PF, dtype: float64 + + >>> import matplotlib.pyplot as plt + >>> pf.kurtosis_rolling(window=12*10).plot() + >>> plt.show() """ return Frame.kurtosis_rolling(self.ror, window=window) @property - def jarque_bera(self): + def jarque_bera(self) -> Dict[str, float]: """ - Performs Jarque-Bera test for normality. - It shows whether the returns have the skewness and kurtosis matching a normal distribution. + Perform Jarque-Bera test for normality of portfolio returns time series. + + Jarque-Bera shows whether the returns have the skewness and kurtosis + matching a normal distribution (null hypothesis or H0). + + Returns + ------- + dict + Jarque-Bera test statistics and p-value. + + Notes + ----- + Test returns statistics (first row) and p-value (second row). + p-value is the probability of obtaining test results, under the assumption that the null hypothesis is correct. + In general, a large Jarque-Bera statistics and tiny p-value indicate that null hypothesis is rejected + and the time series are not normally distributed. - Returns: - (The test statistic, The p-value for the hypothesis test) - Low statistic numbers correspond to normal distribution. + Examples + -------- + >>> pf = ok.Portfolio(['BND.US']) + >>> pf.jarque_bera + {'statistic': 58.27670538027455, 'p-value': 2.2148949341271873e-13} """ return Frame.jarque_bera_series(self.ror) - def kstest(self, distr: str = "norm") -> dict: + def kstest(self, distr: str = "norm") -> Dict[str, float]: """ - Performs Kolmogorov-Smirnov test on portfolio returns and evaluate goodness of fit. - Test works with normal and lognormal distributions. + Perform one sample Kolmogorov-Smirnov test on portfolio returns and evaluate goodness of fit + for a given distribution. + + The one-sample Kolmogorov-Smirnov test compares the rate of return time series against a given distribution. - Returns: - (The test statistic, The p-value for the hypothesis test) + Returns + ------- + dict + Kolmogorov-Smirnov test statistics and p-value. + + Parameters + ---------- + distr : {'norm', 'lognorm'}, default 'norm' + The name of a distribution to fit. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + + + Notes + ----- + Like in Jarque-Bera test returns statistic (first row) and p-value (second row). + Null hypotesis (two distributions are similar) is not rejected when p-value is high enough. + 5% threshold can be used. + + Examples + -------- + >>> pf = ok.Portfolio(['GLD.US']) + >>> pf.kstest(distr='lognorm') + {'statistic': 0.05001344986084533, 'p-value': 0.6799422889377373} + + >>> pf.kstest(distr='norm') + {'statistic': 0.09528000069992831, 'p-value': 0.047761781235967415} + + Kolmogorov-Smirnov test shows that GLD rate of return time series fits lognormal distribution + better than normal one. """ return Frame.kstest_series(self.ror, distr=distr) def plot_percentiles_fit( self, distr: str = "norm", figsize: Optional[tuple] = None - ): + ) -> None: """ - Generates a probability plot of portfolio returns against percentiles of a specified - theoretical distribution (the normal distribution by default). - Works with normal and lognormal distributions. + Generate a quantile-quantile (Q-Q) plot of portfolio monthly rate of return against quantiles of a given + theoretical distribution. + + A q-q plot is a plot of the quantiles of the portfolio rate of return historical data + against the quantiles of a given theoretical distribution. + + Parameters + ---------- + distr : {'norm', 'lognorm'}, default 'norm' + The name of a distribution to fit. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + + figsize : (float, float), optional + Width and height of plot in inches. + If None default matplotlib figsize value is used. + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='year') + >>> pf.plot_percentiles_fit(distr='lognorm') + >>> plt.show() """ plt.figure(figsize=figsize) if distr == "norm": @@ -1803,13 +1796,17 @@ def plot_percentiles_fit( raise ValueError('distr should be "norm" (default) or "lognorm".') plt.show() - def plot_hist_fit(self, distr: str = "norm", bins: int = None): + def plot_hist_fit(self, distr: str = "norm", bins: int = None) -> None: """ - Plots historical distribution histogram and theoretical PDF (Probability Distribution Function). - Lognormal and normal distributions could be used. + Plot historical distribution histogram for ptrtfolio monthly rate of return time series + and theoretical PDF (Probability Distribution Function). - normal distribution - 'norm' - lognormal distribution - 'lognorm' + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> pf = ok.Portfolio(['SP500TR.INDX']) + >>> pf.plot_hist_fit(distr='norm') + >>> plt.show() """ data = self.ror # Plot the histogram @@ -1825,8 +1822,135 @@ def plot_hist_fit(self, distr: str = "norm", bins: int = None): mu = np.log(scale) p = scipy.stats.lognorm.pdf(x, std, loc, scale) else: - raise ValueError('distr should be "norm" (default) or "lognorm".') + raise ValueError('distr must be "norm" (default) or "lognorm".') plt.plot(x, p, "k", linewidth=2) title = "Fit results: mu = %.3f, std = %.3f" % (mu, std) plt.title(title) plt.show() + + def plot_forecast( + self, + distr: str = "norm", + years: int = 5, + percentiles: List[int] = [10, 50, 90], + today_value: Optional[int] = None, + n: int = 1000, + figsize: Optional[tuple] = None, + ) -> plt.axes: + """ + Plot forecasted ranges of wealth indexes (lines) for a given set of percentiles. + Historical wealth index is shown in the same chart. + + Parameters + ---------- + distr : {'hist', 'norm', 'lognorm'}, default 'norm' + Distribution type for the rate of return of portfolio. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + 'hist' - percentiles are taken from the historical data. + + years : int, default 1 + Investment period length to calculate wealth index. + It should not exceed 1/2 of the portfolio history period length 'period_length'. + + percentiles : list of int, default [10, 50, 90] + List of percentiles to be calculated. + + today_value : int, optional + Initial value of the wealth index. + If today_value is None the last value of the historical wealth indexes is taken. It can be useful to plot + the forecast of wealth index togeather with the hitorical data. + + n : int, default 1000 + Number of random time series to generate with Monte Carlo simulation (for 'norm' or 'lognorm' only). + Larger argument values can be used to increase the precision of the calculation. But this will lead + to slower performance. + Is not required for historical distribution (dist='hist'). + + Returns + ------- + Axes : 'matplotlib.axes._subplots.AxesSubplot' + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='year') + >>> pf.plot_forecast() + >>> plt.show() + """ + wealth = self.wealth_index + x1 = self.last_date + x2 = x1.replace(year=x1.year + years) + y_start_value = wealth[self.symbol].iloc[-1] + y_end_values = self.percentile_wealth( + distr=distr, years=years, percentiles=percentiles, n=n + ) + if today_value: + modifier = today_value / y_start_value + wealth *= modifier + y_start_value = y_start_value * modifier + y_end_values.update((x, y * modifier) for x, y in y_end_values.items()) + fig, ax = plt.subplots(figsize=figsize) + ax.plot( + wealth.index.to_timestamp(), + wealth[self.symbol], + linewidth=1, + label="Historical data", + ) + for percentile in percentiles: + x, y = [x1, x2], [y_start_value, y_end_values[percentile]] + if percentile == 50: + ax.plot(x, y, color="blue", linestyle="-", linewidth=2, label="Median") + else: + ax.plot( + x, + y, + linestyle="dashed", + linewidth=1, + label=f"Percentile {percentile}", + ) + ax.legend(loc="upper left") + return ax + + def plot_forecast_monte_carlo( + self, + distr: str = "norm", + years: int = 1, + n: int = 20, + figsize: Optional[tuple] = None, + ) -> None: + """ + Plot Monte Carlo simulation for portfolio wealth indexes together with historical wealth index. + + Random wealth indexes are generated according to a given distribution. + + Parameters + ---------- + distr : {'norm', 'lognorm'}, default 'norm' + Distribution type for the rate of return of portfolio. + 'norm' - for normal distribution. + 'lognorm' - for lognormal distribution. + + years : int, default 1 + Investment period length for new wealth indexes + It should not exceed 1/2 of the portfolio history period length 'period_length'. + + n : int, default 20 + Number of random wealth indexes to generate with Monte Carlo simulation. + + figsize : (float, float), optional + Width, height in inches. + If None default matplotlib figsize value is used. + + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='year') + >>> pf.plot_forecast_monte_carlo(years=5, distr='lognorm', n=100) + >>> plt.show() + """ + s1 = self.wealth_index + s2 = self._monte_carlo_wealth(distr=distr, years=years, n=n) + s1[self.symbol].plot(legend=None, figsize=figsize) + for n in s2: + s2[n].plot(legend=None)