diff --git a/openbb_terminal/core/sdk/models/econometrics_sdk_model.py b/openbb_terminal/core/sdk/models/econometrics_sdk_model.py index a532ac86145f..2bd9a60a11df 100644 --- a/openbb_terminal/core/sdk/models/econometrics_sdk_model.py +++ b/openbb_terminal/core/sdk/models/econometrics_sdk_model.py @@ -22,6 +22,8 @@ class EconometricsRoot(Category): `dwat_chart`: Show Durbin-Watson autocorrelation tests\n `fdols`: First differencing is an alternative to using fixed effects when there might be correlation.\n `fe`: When effects are correlated with the regressors the RE and BE estimators are not consistent.\n + `garch`: Calculates annualized volatility forecasts based on GARCH.\n + `garch_chart`: Plots the annualized volatility forecasts based on GARCH\n `get_regression_data`: This function creates a DataFrame with the required regression data as\n `granger`: Calculate granger tests\n `granger_chart`: Show granger tests\n @@ -56,6 +58,8 @@ def __init__(self): self.dwat_chart = lib.econometrics_regression_view.display_dwat self.fdols = lib.econometrics_regression_model.get_fdols self.fe = lib.econometrics_regression_model.get_fe + self.garch = lib.econometrics_model.get_garch + self.garch_chart = lib.econometrics_view.display_garch self.get_regression_data = lib.econometrics_regression_model.get_regression_data self.granger = lib.econometrics_model.get_granger_causality self.granger_chart = lib.econometrics_view.display_granger diff --git a/openbb_terminal/core/sdk/trail_map.csv b/openbb_terminal/core/sdk/trail_map.csv index 042188073640..5809887f91fd 100644 --- a/openbb_terminal/core/sdk/trail_map.csv +++ b/openbb_terminal/core/sdk/trail_map.csv @@ -183,6 +183,7 @@ econometrics.panel,econometrics_regression_model.get_regressions_results,econome econometrics.pols,econometrics_regression_model.get_pols, econometrics.re,econometrics_regression_model.get_re, econometrics.root,econometrics_model.get_root,econometrics_view.display_root +econometrics.garch,econometrics_model.get_garch,econometrics_view.display_garch economy.available_indices,economy_yfinance_model.get_available_indices, economy.balance,economy_oecd_model.get_balance,economy_oecd_view.plot_balance economy.bigmac,economy_nasdaq_model.get_big_mac_indices,economy_nasdaq_view.display_big_mac_index diff --git a/openbb_terminal/econometrics/econometrics_controller.py b/openbb_terminal/econometrics/econometrics_controller.py index a88fa67be37a..8602eeabec2b 100644 --- a/openbb_terminal/econometrics/econometrics_controller.py +++ b/openbb_terminal/econometrics/econometrics_controller.py @@ -65,6 +65,7 @@ class EconometricsController(BaseController): "combine", "rename", "lag", + "ret", "ols", "norm", "root", @@ -73,6 +74,7 @@ class EconometricsController(BaseController): "dwat", "bgod", "bpag", + "garch", "granger", "coint", ] @@ -181,6 +183,7 @@ def __init__(self, queue: Optional[List[str]] = None): "plot", "norm", "root", + "garch", "granger", "coint", "corr", @@ -217,6 +220,7 @@ def update_runtime_choices(self): "ols", "panel", "delete", + "garch", ]: self.choices[feature] = dataset_columns for feature in [ @@ -280,17 +284,19 @@ def print_help(self): mt.add_cmd("combine", self.files) mt.add_cmd("rename", self.files) mt.add_cmd("lag", self.files) + mt.add_cmd("ret", self.files) mt.add_cmd("export", self.files) - mt.add_info("_tests_") + mt.add_info("time_series_") mt.add_cmd("norm", self.files) - mt.add_cmd("root", self.files) + mt.add_cmd("ols", self.files) mt.add_cmd("granger", self.files) + mt.add_cmd("root", self.files) mt.add_cmd("coint", self.files) - mt.add_info("_regression_") - mt.add_cmd("ols", self.files) + mt.add_cmd("garch", self.files) + mt.add_info("_panel_") mt.add_cmd("panel", self.files) mt.add_cmd("compare", self.files) - mt.add_info("_regression_tests_") + mt.add_info("_residuals_") mt.add_cmd("dwat", self.files and self.regression["OLS"]["model"]) mt.add_cmd("bgod", self.files and self.regression["OLS"]["model"]) mt.add_cmd("bpag", self.files and self.regression["OLS"]["model"]) @@ -1224,6 +1230,50 @@ def call_lag(self, other_args: List[str]): self.update_runtime_choices() + @log_start_end(log=logger) + def call_ret(self, other_args: List[str]): + """Process ret command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="ret", + description="Calculate returns for the given column.", + ) + parser.add_argument( + "-v", + "--values", + help="Dataset.column values to calculate returns.", + dest="values", + choices={ + f"{dataset}.{column}": {column: None, dataset: None} + for dataset, dataframe in self.datasets.items() + for column in dataframe.columns + }, + type=str, + required="-h" not in other_args, + ) + + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-v") + ns_parser = self.parse_known_args_and_warn( + parser, other_args, export_allowed=NO_EXPORT + ) + + if not ns_parser: + return + + try: + dataset, col = ns_parser.values.split(".") + data = self.datasets[dataset] + except ValueError: + console.print("[red]Please enter 'dataset'.'column'.[/red]\n") + return + + data[col + "_returns"] = econometrics_model.get_returns(data[col]) + self.datasets[dataset] = data + + self.update_runtime_choices() + @log_start_end(log=logger) def call_eval(self, other_args): """Process eval command""" @@ -1890,6 +1940,124 @@ def call_bpag(self, other_args): self.regression["OLS"]["model"], ns_parser.export ) + @log_start_end(log=logger) + def call_garch(self, other_args: List[str]): + """Process garch command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="garch", + description=r"""Calculates annualized volatility forecasts based on GARCH. + GARCH (Generalized autoregressive conditional heteroskedasticity) is stochastic model for time series, + which is for instance used to model volatility clusters, stock return and inflation. It is a + generalisation of the ARCH models. + + $\text{GARCH}(p, q) = (1 - \alpha - \beta) \sigma_l + \sum_{i=1}^q \alpha u_{t-i}^2 + \sum_{i=1}^p \beta + \sigma_{t-i}^2$ [1] + + The GARCH-model assumes that the variance estimate consists of 3 components: + - $\sigma_l$ ; the long term component, which is unrelated to the current market conditions + - $u_t$ ; the innovation/discovery through current market price changes + - $\sigma_t$ ; the last estimate + + GARCH can be understood as a model, which allows to optimize these 3 variance components to the time + series. This is done assigning weights to variance components: $(1 - \alpha - \beta)$ for $\sigma_l$ , + $\alpha$ for $u_t$ and $\beta$ for $\sigma_t$ . [2] + + The weights can be estimated by iterating over different values of $(1 - \alpha - \beta) \sigma_l$ + which we will call $\omega$ , $\alpha$ and $\beta$ , while maximizing: + $\sum_{i} -ln(v_i) - (u_i ^ 2) / v_i$ . With the constraints: + - $\alpha > 0$ + - $\beta > 0$ + - $\alpha + \beta < 1$ + Note that there is no restriction on $\omega$ . + + Another method used for estimation is "variance targeting", where one first sets $\omega$ + equal to the variance of the time series. This method nearly as effective as the previously mentioned and + is less computationally effective. + + One can measure the fit of the time series to the GARCH method by using the Ljung-Box statistic. [3] + + See the sources below for reference and for greater detail. + + Sources: + [1] Generalized Autoregressive Conditional Heteroskedasticity, by Tim Bollerslev + [2] Finance Compact Plus Band 1, by Yvonne Seler Zimmerman and Heinz Zimmerman; ISBN: 978-3-907291-31-1 + [3] Options, Futures & other Derivates, by John C. Hull; ISBN: 0-13-022444-8""", + ) + parser.add_argument( + "-v", + "--value", + type=str, + choices=self.choices["garch"], + dest="column", + help="The column and name of the database you want to estimate volatility for", + required="-h" not in other_args, + ) + parser.add_argument( + "-p", + help="The lag order of the symmetric innovation", + dest="p", + type=int, + default=1, + ) + parser.add_argument( + "-o", + help="The lag order of the asymmetric innovation", + dest="o", + type=int, + default=0, + ) + parser.add_argument( + "-q", + help="The lag order of lagged volatility or equivalent", + dest="q", + type=int, + default=1, + ) + parser.add_argument( + "-m", + "--mean", + help="Choose mean model", + choices=["LS", "AR", "ARX", "HAR", "HARX", "constant", "zero"], + default="constant", + type=str, + dest="mean", + ) + parser.add_argument( + "-l", + "--length", + help="The length of the estimate", + dest="horizon", + type=int, + default=100, + ) + parser.add_argument( + "-d", + "--detailed", + help="Display the details about the parameter fit, for instance the confidence interval", + dest="detailed", + action="store_true", + default=False, + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-v") + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + dataset, column = ns_parser.column.split(".") + econometrics_view.display_garch( + self.datasets[dataset], + column, + ns_parser.p, + ns_parser.o, + ns_parser.q, + ns_parser.mean, + ns_parser.horizon, + ns_parser.detailed, + ) + @log_start_end(log=logger) def call_granger(self, other_args: List[str]): """Process granger command""" diff --git a/openbb_terminal/econometrics/econometrics_model.py b/openbb_terminal/econometrics/econometrics_model.py index 60809a343dc2..fdcabb4c68ef 100644 --- a/openbb_terminal/econometrics/econometrics_model.py +++ b/openbb_terminal/econometrics/econometrics_model.py @@ -8,18 +8,18 @@ from itertools import combinations from typing import Any, Dict, Optional, Tuple, Union +import numpy as np import pandas as pd import statsmodels.api as sm +from arch import arch_model from scipy import stats from statsmodels.tsa.stattools import adfuller, grangercausalitytests, kpss -from openbb_terminal.decorators import log_start_end from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) -@log_start_end(log=logger) def get_options( datasets: Dict[str, pd.DataFrame], dataset_name: str = "" ) -> Dict[Union[str, Any], pd.DataFrame]: @@ -61,7 +61,6 @@ def get_options( return option_tables -@log_start_end(log=logger) def get_corr_df(data: pd.DataFrame) -> pd.DataFrame: """Returns correlation for a given df @@ -79,7 +78,6 @@ def get_corr_df(data: pd.DataFrame) -> pd.DataFrame: return corr -@log_start_end(log=logger) def clean( dataset: pd.DataFrame, fill: str = "", @@ -127,7 +125,6 @@ def clean( return dataset -@log_start_end(log=logger) def get_normality(data: pd.Series) -> pd.DataFrame: """ The distribution of returns and generate statistics on the relation to the normal curve. @@ -182,7 +179,6 @@ def get_normality(data: pd.Series) -> pd.DataFrame: ) -@log_start_end(log=logger) def get_root( data: pd.Series, fuller_reg: str = "c", kpss_reg: str = "c" ) -> pd.DataFrame: @@ -224,10 +220,9 @@ def get_root( return data -@log_start_end(log=logger) def get_granger_causality( dependent_series: pd.Series, independent_series: pd.Series, lags: int = 3 -) -> dict: +) -> pd.DataFrame: """Calculate granger tests Parameters @@ -241,8 +236,8 @@ def get_granger_causality( Returns ------- - dict - Dictionary containing results of Granger test + pd.DataFrame + Dataframe containing results of Granger test """ granger_set = pd.concat([dependent_series, independent_series], axis=1) @@ -326,6 +321,90 @@ def get_coint_df( return pd.DataFrame() +def get_returns(data: pd.Series): + """Calculate returns for the given time series + + Parameters + ---------- + data: pd.Series + The data series to calculate returns for + """ + return 100 * data.pct_change().dropna() + + +def get_garch( + data: pd.Series, + p: int = 1, + o: int = 0, + q: int = 1, + mean: str = "constant", + horizon: int = 100, +): + r"""Calculates volatility forecasts based on GARCH. + + GARCH (Generalized autoregressive conditional heteroskedasticity) is stochastic model for time series, + which is for instance used to model volatility clusters, stock return and inflation. It is a + generalisation of the ARCH models. + + $ GARCH(p, q) = (1 - \alpha - \beta) \sigma_l + \sum_{i=1}^q \alpha u_{t-i}^2 + \sum_{i=1}^p \beta \sigma_{t-i}^2 + $ [1] + + The GARCH-model assumes that the variance estimate consists of 3 components: + - $ \sigma_l $; the long term component, which is unrelated to the current market conditions + - $ u_t $; the innovation/discovery through current market price changes + - $ \sigma_t $; the last estimate + + GARCH can be understood as a model, which allows to optimize these 3 variance components to the time series. + This is done assigning weights to variance components: $ (1 - \alpha - \beta) $ for $ \sigma_l $, $ \alpha $ for + $ u_t $ and $ \beta $ for $ \sigma_t $. [2] + + The weights can be estimated by iterating over different values of $ (1 - \alpha - \beta) \sigma_l $ which we + will call $ \omega $, $ \alpha $ and $ \beta $, while maximizing: $ \sum_{i} -ln(v_i) - (u_i ^ 2) / v_i $. + With the constraints: + - $ \alpha > 0 $ + - $ \beta > 0 $ + - $ \alpha + \beta < 1 $ + Note that there is no restriction on $ \omega $. + + Another method used for estimation is "variance targeting", where one first sets $ \omega $ + equal to the variance of the time series. This method nearly as effective as the previously mentioned and + is less computationally effective. + + One can measure the fit of the time series to the GARCH method by using the Ljung-Box statistic. [3] + + See the sources below for reference and for greater detail. + + Sources: + [1] Generalized Autoregressive Conditional Heteroskedasticity, by Tim Bollerslev + [2] Finance Compact Plus Band 1, by Yvonne Seler Zimmerman and Heinz Zimmerman; ISBN: 978-3-907291-31-1 + [3] Options, Futures & other Derivates, by John C. Hull; ISBN: 0-13-022444-8 + + Parameters + ---------- + data: pd.Series + The time series (often returns) to estimate volatility from + p: int + Lag order of the symmetric innovation + o: int + Lag order of the asymmetric innovation + q: int + Lag order of lagged volatility or equivalent + mean: str + The name of the mean model + horizon: int + The horizon of the forecast + Examples + -------- + >>> from openbb_terminal.sdk import openbb + >>> openbb.econometrics.garch(openbb.stocks.load("AAPL").iloc[:, 0].pct_change()*100) + """ + model = arch_model(data.dropna(), vol="GARCH", p=p, o=o, q=q, mean=mean) + model_fit = model.fit(disp="off") + pred = model_fit.forecast(horizon=horizon, reindex=False) + + return np.sqrt(pred.variance.values[-1, :]), model_fit + + def get_engle_granger_two_step_cointegration_test( dependent_series: pd.Series, independent_series: pd.Series ) -> Tuple[float, float, float, pd.Series, float, float]: diff --git a/openbb_terminal/econometrics/econometrics_view.py b/openbb_terminal/econometrics/econometrics_view.py index 2f80a9f0f543..c13342c89440 100644 --- a/openbb_terminal/econometrics/econometrics_view.py +++ b/openbb_terminal/econometrics/econometrics_view.py @@ -1,6 +1,8 @@ """Econometrics View""" __docformat__ = "numpy" +# pylint: disable=too-many-arguments + import logging import os from typing import Dict, Optional, Union @@ -43,7 +45,9 @@ def show_options( "Please load in a dataset by using the 'load' command before using this feature." ) else: - option_tables = econometrics_model.get_options(datasets, dataset_name) + option_tables = econometrics_model.get_options( + datasets, dataset_name if dataset_name is not None else "" + ) for dataset, data_values in option_tables.items(): print_rich_table( @@ -386,6 +390,77 @@ def display_root( ) +@log_start_end(log=logger) +def display_garch( + dataset: pd.DataFrame, + column: str, + p: int = 1, + o: int = 0, + q: int = 1, + mean: str = "constant", + horizon: int = 1, + detailed: bool = False, + export: str = "", + external_axes: bool = False, +) -> Union[OpenBBFigure, None]: + """Plots the volatility forecasts based on GARCH + + Parameters + ---------- + dataset: pd.DataFrame + The dataframe to use + column: str + The column of the dataframe to use + p: int + Lag order of the symmetric innovation + o: int + Lag order of the asymmetric innovation + q: int + Lag order of lagged volatility or equivalent + mean: str + The name of the mean model + horizon: int + The horizon of the forecast + detailed: bool + Whether to display the details about the parameter fit, for instance the confidence interval + export: str + Format to export data + external_axes: bool + Whether to return the figure object or not, by default False + """ + data = dataset[column] + result, garch_fit = econometrics_model.get_garch(data, p, o, q, mean, horizon) + + fig = OpenBBFigure() + + fig.add_scatter(x=list(range(1, horizon + 1)), y=result) + fig.set_title( + f"{f'GARCH({p}, {o}, {q})' if o != 0 else f'GARCH({p}, {q})'} volatility forecast" + ) + + if fig.is_image_export(export): + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + f"{column}_{dataset}_GARCH({p},{q})", + result, + figure=fig, + ) + + if not detailed: + print_rich_table( + garch_fit.params.to_frame(), + headers=["Values"], + show_index=True, + index_name="Parameters", + title=f"GARCH({p}, {o}, {q})" if o != 0 else f"GARCH({p}, {q})", + export=bool(export), + ) + else: + console.print(garch_fit) + return fig.show(external=external_axes) + + @log_start_end(log=logger) def display_granger( dependent_series: pd.Series, diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 6cabec05ecef..f06570a2d4f5 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -966,19 +966,21 @@ en: econometrics/combine: Combine columns from different datasets econometrics/rename: Rename column from dataset econometrics/lag: Add lag to a variable by shifting a column - econometrics/_regression_: Regression - econometrics/_regression_tests_: Regression Tests + econometrics/_panel_: Panel + econometrics/_residuals_: Residuals econometrics/ols: fit a (multi) linear regression model econometrics/norm: perform normality tests on a column of a dataset econometrics/root: perform unitroot tests (ADF & KPSS) on a column of a dataset econometrics/panel: estimate model based on various regression techniques econometrics/compare: compare results of all estimated models - econometrics/_tests_: Tests + econometrics/_time_series_: Time Series econometrics/dwat: Durbin-Watson autocorrelation test on the residuals of the regression econometrics/bgod: Breusch-Godfrey autocorrelation tests with lags on the residuals of the regression econometrics/bpag: Breusch-Pagan heteroscedasticity test on the residuals of the regression econometrics/granger: Granger causality tests on two columns econometrics/coint: co-integration test on a multitude of columns + econometrics/garch: estimate future volatility with GARCH + econometrics/ret: calculate returns for the given time series portfolio/bro: brokers holdings supports robinhood, ally, degiro, coinbase portfolio/po: portfolio optimization optimize your portfolio weights efficiently portfolio/load: load transactions into the portfolio (use load --example for an example) diff --git a/openbb_terminal/miscellaneous/integration_tests_scripts/econometrics/test_econometrics.openbb b/openbb_terminal/miscellaneous/integration_tests_scripts/econometrics/test_econometrics.openbb index 1f805c285725..2e0ae729b31c 100644 --- a/openbb_terminal/miscellaneous/integration_tests_scripts/econometrics/test_econometrics.openbb +++ b/openbb_terminal/miscellaneous/integration_tests_scripts/econometrics/test_econometrics.openbb @@ -3,6 +3,8 @@ econometrics ## Exploration load nile +ret -v nile.volume +garch -v nile.volume_returns desc nile load nile -a nile_2 eval double_volume = volume * 2 diff --git a/openbb_terminal/sdk.py b/openbb_terminal/sdk.py index 772854ee3627..3dc04b8727fe 100644 --- a/openbb_terminal/sdk.py +++ b/openbb_terminal/sdk.py @@ -123,6 +123,8 @@ def econometrics(self): `dwat_chart`: Show Durbin-Watson autocorrelation tests\n `fdols`: First differencing is an alternative to using fixed effects when there might be correlation.\n `fe`: When effects are correlated with the regressors the RE and BE estimators are not consistent.\n + `garch`: Calculates annualized volatility forecasts based on GARCH.\n + `garch_chart`: Plots the annualized volatility forecasts based on GARCH\n `get_regression_data`: This function creates a DataFrame with the required regression data as\n `granger`: Calculate granger tests\n `granger_chart`: Show granger tests\n diff --git a/requirements.txt b/requirements.txt index 01110416d235..ead630034e4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ altair==4.2.2 ; python_version >= "3.8" and python_full_version != "3.9.7" and p ansiwrap==0.8.4 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" anyio==3.6.2 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" appdirs==1.4.4 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" +arch==5.3.1 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" appnope==0.1.3 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" and sys_platform == "darwin" or python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" and platform_system == "Darwin" argon2-cffi-bindings==21.2.0 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11" argon2-cffi==21.3.0 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"