diff --git a/openbb_terminal/common/technical_analysis/volatility_model.py b/openbb_terminal/common/technical_analysis/volatility_model.py index e0036ac7855..e8a1ed0c45d 100644 --- a/openbb_terminal/common/technical_analysis/volatility_model.py +++ b/openbb_terminal/common/technical_analysis/volatility_model.py @@ -13,6 +13,8 @@ from openbb_terminal.decorators import log_start_end logger = logging.getLogger(__name__) MAMODES = ["ema", "sma", "wma", "hma", "zlma"] + +# These are parameters for the volatility models VOLATILITY_MODELS = [ "STD", "Parkinson", @@ -177,6 +179,339 @@ def atr( ) +@log_start_end(log=logger) +def standard_deviation( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = False, + clean: bool = True, +) -> pd.DataFrame: + """Standard deviation measures how widely returns are dispersed from the average return. + It is the most common (and biased) estimator of volatility. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.DataFrame : results + Dataframe with results. + + Example + ------- + >>> data = openbb.stocks.load('SPY') + >>> df = openbb.ta.standard_deviation(data) + """ + + trading_periods = 365 if is_crypto else 252 + + log_return = (data["Close"] / data["Close"].shift(1)).apply(np.log) + + result = log_return.rolling(window=window, center=False).std() * np.sqrt( + trading_periods + ) + + if clean: + return result.dropna() + + return result + + +@log_start_end(log=logger) +def parkinson( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = False, + clean=True, +) -> pd.DataFrame: + """Parkinson volatility uses the high and low price of the day rather than just close to close prices. + It is useful for capturing large price movements during the day. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.DataFrame : results + Dataframe with results. + + Example + ------- + >>> data = openbb.stocks.load('BTC-USD') + >>> df = openbb.ta.parkinson(data, is_crypto = True) + """ + + trading_periods = 365 if is_crypto else 252 + + rs = (1.0 / (4.0 * np.log(2.0))) * ( + (data["High"] / data["Low"]).apply(np.log) + ) ** 2.0 + + def f(v): + return (trading_periods * v.mean()) ** 0.5 + + result = rs.rolling(window=window, center=False).apply(func=f) + + if clean: + return result.dropna() + + return result + + +@log_start_end(log=logger) +def garman_klass( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = False, + clean=True, +) -> pd.DataFrame: + """Garman-Klass volatility extends Parkinson volatility by taking into account the opening and closing price. + As markets are most active during the opening and closing of a trading session. + It makes volatility estimation more accurate. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.DataFrame : results + Dataframe with results. + + Example + ------- + >>> data = openbb.stocks.load('BTC-USD') + >>> df = openbb.ta.garman_klass(data, is_crypto = True) + """ + + trading_periods = 365 if is_crypto else 252 + + log_hl = (data["High"] / data["Low"]).apply(np.log) + log_co = (data["Close"] / data["Open"]).apply(np.log) + + rs = 0.5 * log_hl**2 - (2 * np.log(2) - 1) * log_co**2 + + def f(v): + return (trading_periods * v.mean()) ** 0.5 + + result = rs.rolling(window=window, center=False).apply(func=f) + + if clean: + return result.dropna() + + return result + + +@log_start_end(log=logger) +def hodges_tompkins( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = False, + clean=True, +) -> pd.DataFrame: + """Hodges-Tompkins volatility is a bias correction for estimation using an overlapping data sample. + It produces unbiased estimates and a substantial gain in efficiency. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.DataFrame : results + Dataframe with results. + + Example + ------- + >>> data = openbb.stocks.load('BTC-USD') + >>> df = openbb.ta.hodges_tompkins(data, is_crypto = True) + """ + + trading_periods = 365 if is_crypto else 252 + + log_return = (data["Close"] / data["Close"].shift(1)).apply(np.log) + + vol = log_return.rolling(window=window, center=False).std() * np.sqrt( + trading_periods + ) + + h = window + n = (log_return.count() - h) + 1 + + adj_factor = 1.0 / (1.0 - (h / n) + ((h**2 - 1) / (3 * n**2))) + + result = vol * adj_factor + + if clean: + return result.dropna() + + return result + + +@log_start_end(log=logger) +def rogers_satchell( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = False, + clean=True, +) -> pd.Series: + """Rogers-Satchell is an estimator for measuring the volatility with an average return not equal to zero. + Unlike Parkinson and Garman-Klass estimators, Rogers-Satchell incorporates a drift term, + mean return not equal to zero. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.Series : results + Pandas Series with results. + + Example + ------- + >>> data = openbb.stocks.load('BTC-USD') + >>> df = openbb.ta.rogers_satchell(data, is_crypto = True) + """ + + trading_periods = 365 if is_crypto else 252 + + log_ho = (data["High"] / data["Open"]).apply(np.log) + log_lo = (data["Low"] / data["Open"]).apply(np.log) + log_co = (data["Close"] / data["Open"]).apply(np.log) + + rs = log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co) + + def f(v): + return (trading_periods * v.mean()) ** 0.5 + + result = rs.rolling(window=window, center=False).apply(func=f) + + if clean: + return result.dropna() + + return result + + +def yang_zhang( + data: pd.DataFrame, + window: int = 30, + trading_periods: int = 252, + is_crypto: bool = True, + clean=True, +) -> pd.DataFrame: + """Yang-Zhang volatility is the combination of the overnight (close-to-open volatility). + It is a weighted average of the Rogers-Satchell volatility and the open-to-close volatility. + + Parameters + ---------- + data : pd.DataFrame + Dataframe of OHLC prices. + window : int [default: 30] + Length of window to calculate standard deviation. + trading_periods : int [default: 252] + Number of trading periods in a year. + is_crypto : bool [default: False] + If true, trading_periods is defined as 365. + clean : bool [default: True] + Whether to clean the data or not by dropping NaN values. + + Returns + ------- + pd.DataFrame : results + Dataframe with results. + + Example + ------- + >>> data = openbb.stocks.load('BTC-USD') + >>> df = openbb.ta.yang_zhang(data, is_crypto = True) + """ + + trading_periods = 365 if is_crypto else 252 + + log_ho = (data["High"] / data["Open"]).apply(np.log) + log_lo = (data["Low"] / data["Open"]).apply(np.log) + log_co = (data["Close"] / data["Open"]).apply(np.log) + + log_oc = (data["Open"] / data["Close"].shift(1)).apply(np.log) + log_oc_sq = log_oc**2 + + log_cc = (data["Close"] / data["Close"].shift(1)).apply(np.log) + log_cc_sq = log_cc**2 + + rs = log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co) + + close_vol = log_cc_sq.rolling(window=window, center=False).sum() * ( + 1.0 / (window - 1.0) + ) + open_vol = log_oc_sq.rolling(window=window, center=False).sum() * ( + 1.0 / (window - 1.0) + ) + window_rs = rs.rolling(window=window, center=False).sum() * (1.0 / (window - 1.0)) + + k = 0.34 / (1.34 + (window + 1) / (window - 1)) + result = (open_vol + k * close_vol + (1 - k) * window_rs).apply(np.sqrt) * np.sqrt( + trading_periods + ) + + if clean: + return result.dropna() + + return result + + @log_start_end(log=logger) def cones( data: pd.DataFrame, @@ -228,11 +563,21 @@ def cones( Example ------- - df = openbb.stocks.load("AAPL") - cones_df = openbb.ta.cones(data = df, lower_q = 0.10, upper_q = 0.90) - """ + >>> df = openbb.stocks.load("AAPL") + >>> cones_df = openbb.ta.cones(data = df, lower_q = 0.10, upper_q = 0.90) - n_days: int = 365 if is_crypto else 252 + >>> cones_df = openbb.ta.cones(df,0.15,0.85,False,"Garman-Klass") + + """ + estimator = pd.DataFrame() + + if lower_q > upper_q: + lower_q, upper_q = upper_q, lower_q + + if (lower_q >= 1) or (upper_q >= 1): + print("Error: lower_q and upper_q must be between 0 and 1") + cones_df = pd.DataFrame() + return cones_df try: lower_q_label = str(int(lower_q * 100)) @@ -247,152 +592,28 @@ def cones( realized = [] data = data.sort_index(ascending=False) - def standard_deviation(data, window=30, trading_periods=n_days, clean=True): - """Standard deviation measures how widely returns are dispersed from the average return. - It is the most common (and biased) estimator of volatility.""" - log_return = (data["Close"] / data["Close"].shift(1)).apply(np.log) - - result = log_return.rolling(window=window, center=False).std() * np.sqrt( - trading_periods - ) - - if clean: - return result.dropna() - - return result - - def parkinson(data, window=30, trading_periods=n_days, clean=True): - """Parkinson volatility uses the high and low price of the day rather than just close to close prices. - It is useful for capturing large price movements during the day.""" - rs = (1.0 / (4.0 * np.log(2.0))) * ( - (data["High"] / data["Low"]).apply(np.log) - ) ** 2.0 - - def f(v): - return (trading_periods * v.mean()) ** 0.5 - - result = rs.rolling(window=window, center=False).apply(func=f) - - if clean: - return result.dropna() - - return result - - def garman_klass(data, window=30, trading_periods=n_days, clean=True): - """Garman-Klass volatility extends Parkinson volatility by taking into account the opening and closing price. - As markets are most active during the opening and closing of a trading session. - It makes volatility estimation more accurate. - """ - log_hl = (data["High"] / data["Low"]).apply(np.log) - log_co = (data["Close"] / data["Open"]).apply(np.log) - - rs = 0.5 * log_hl**2 - (2 * np.log(2) - 1) * log_co**2 - - def f(v): - return (trading_periods * v.mean()) ** 0.5 - - result = rs.rolling(window=window, center=False).apply(func=f) - - if clean: - return result.dropna() - - return result - - def hodges_tompkins(data, window=30, trading_periods=n_days, clean=True): - """Hodges-Tompkins volatility is a bias correction for estimation using an overlapping data sample. - It produces unbiased estimates and a substantial gain in efficiency.""" - log_return = (data["Close"] / data["Close"].shift(1)).apply(np.log) - - vol = log_return.rolling(window=window, center=False).std() * np.sqrt( - trading_periods - ) - - h = window - n = (log_return.count() - h) + 1 - - adj_factor = 1.0 / (1.0 - (h / n) + ((h**2 - 1) / (3 * n**2))) - - result = vol * adj_factor - - if clean: - return result.dropna() - - return result - - def rogers_satchell(data, window=30, trading_periods=n_days, clean=True): - """Rogers-Satchell is an estimator for measuring the volatility with an average return not equal to zero. - Unlike Parkinson and Garman-Klass estimators, Rogers-Satchell incorporates a drift term, - mean return not equal to zero. - """ - - log_ho = (data["High"] / data["Open"]).apply(np.log) - log_lo = (data["Low"] / data["Open"]).apply(np.log) - log_co = (data["Close"] / data["Open"]).apply(np.log) - - rs = log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co) - - def f(v): - return (trading_periods * v.mean()) ** 0.5 - - result = rs.rolling(window=window, center=False).apply(func=f) - - if clean: - return result.dropna() - - return result - - def yang_zhang(data, window=30, trading_periods=n_days, clean=True): - """Yang-Zhang volatility is the combination of the overnight (close-to-open volatility). - It is a weighted average of the Rogers-Satchell volatility and the open-to-close volatility. - """ - log_ho = (data["High"] / data["Open"]).apply(np.log) - log_lo = (data["Low"] / data["Open"]).apply(np.log) - log_co = (data["Close"] / data["Open"]).apply(np.log) - - log_oc = (data["Open"] / data["Close"].shift(1)).apply(np.log) - log_oc_sq = log_oc**2 - - log_cc = (data["Close"] / data["Close"].shift(1)).apply(np.log) - log_cc_sq = log_cc**2 - - rs = log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co) - - close_vol = log_cc_sq.rolling(window=window, center=False).sum() * ( - 1.0 / (window - 1.0) - ) - open_vol = log_oc_sq.rolling(window=window, center=False).sum() * ( - 1.0 / (window - 1.0) - ) - window_rs = rs.rolling(window=window, center=False).sum() * ( - 1.0 / (window - 1.0) - ) - - k = 0.34 / (1.34 + (window + 1) / (window - 1)) - result = (open_vol + k * close_vol + (1 - k) * window_rs).apply( - np.sqrt - ) * np.sqrt(trading_periods) - - if clean: - return result.dropna() - - return result - for window in windows: # Looping to build a dataframe with realized volatility over each window. if model not in VOLATILITY_MODELS: print("Model not available. Available models: ", VOLATILITY_MODELS) elif model == "STD": - estimator = standard_deviation(window=window, data=data) + estimator = standard_deviation( + window=window, data=data, is_crypto=is_crypto + ) elif model == "Parkinson": - estimator = parkinson(window=window, data=data) + estimator = parkinson(window=window, data=data, is_crypto=is_crypto) elif model == "Garman-Klass": - estimator = garman_klass(window=window, data=data) + estimator = garman_klass(window=window, data=data, is_crypto=is_crypto) elif model == "Hodges-Tompkins": - estimator = hodges_tompkins(window=window, data=data) + estimator = hodges_tompkins( + window=window, data=data, is_crypto=is_crypto + ) elif model == "Rogers-Satchell": - estimator = rogers_satchell(window=window, data=data) + estimator = rogers_satchell( + window=window, data=data, is_crypto=is_crypto + ) elif model == "Yang-Zhang": - estimator = yang_zhang(window=window, data=data) + estimator = yang_zhang(window=window, data=data, is_crypto=is_crypto) min_.append(estimator.min()) max_.append(estimator.max()) median.append(estimator.median()) @@ -417,8 +638,4 @@ def cones( except Exception: cones_df = pd.DataFrame() - if lower_q or upper_q > 0: - print( - "Upper and lower quantiles should be expressed as a value between 0 and 1" - ) return cones_df diff --git a/openbb_terminal/common/technical_analysis/volatility_view.py b/openbb_terminal/common/technical_analysis/volatility_view.py index 41eae464361..3abccdd1368 100644 --- a/openbb_terminal/common/technical_analysis/volatility_view.py +++ b/openbb_terminal/common/technical_analysis/volatility_view.py @@ -378,7 +378,6 @@ def display_cones( is_crypto: bool = False, export: str = "", sheet_name: Optional[str] = None, - external_axes: Optional[List[plt.Axes]] = None, ): """Plots the realized volatility quantiles for the loaded ticker. The model used to calculate the volatility is selectable. @@ -422,8 +421,6 @@ def display_cones( Format of export file sheet_name: str Optionally specify the name of the sheet the data is exported to. - external_axes : Optional[List[plt.Axes]], optional - External axes (1 axis is expected in the list), by default None Examples -------- @@ -473,8 +470,7 @@ def display_cones( plt.xticks(df_ta.index) plt.tight_layout(pad=2.0) - if external_axes is None: - theme.visualize_output() + theme.visualize_output() export_data( export, diff --git a/openbb_terminal/core/sdk/models/ta_sdk_model.py b/openbb_terminal/core/sdk/models/ta_sdk_model.py index 668cecbe134..692602c3260 100644 --- a/openbb_terminal/core/sdk/models/ta_sdk_model.py +++ b/openbb_terminal/core/sdk/models/ta_sdk_model.py @@ -47,6 +47,12 @@ class TaRoot(Category): `obv_chart`: Plots OBV technical indicator\n `rsi`: Relative strength index\n `rsi_chart`: Plots RSI Indicator\n + `rvol_std`: Standard deviation model for realized volatility.\n + `rvol_parkinson`: Parkinson's model for realized volatility.\n + `rvol_garman_klass`: Garman-Klass model for realized volatility.\n + `rvol_hodges_tompkins`: Hodges-Tompkins model for realized volatility.\n + `rvol_rogers-satchell`: Rogers-Satterthwaite model for realized volatility.\n + `rvol_yang_zhang`: Yang-Zhang model for realized volatility.\n `cones`: Realized Volatility Cones\n `cones_chart`: Plots Realized Volatility Cones\n `sma`: Gets simple moving average (SMA) for stock\n @@ -100,6 +106,12 @@ class TaRoot(Category): self.obv_chart = lib.common_ta_volume_view.display_obv self.rsi = lib.common_ta_momentum_model.rsi self.rsi_chart = lib.common_ta_momentum_view.display_rsi + self.rvol_std = lib.common_ta_volatility_model.standard_deviation + self.rvol_parkinson = lib.common_ta_volatility_model.parkinson + self.rvol_garman_klass = lib.common_ta_volatility_model.garman_klass + self.rvol_hodges_tompkins = lib.common_ta_volatility_model.hodges_tompkins + self.rvol_rogers_satchell = lib.common_ta_volatility_model.rogers_satchell + self.rvol_yang_zhang = lib.common_ta_volatility_model.yang_zhang self.cones = lib.common_ta_volatility_model.cones self.cones_chart = lib.common_ta_volatility_view.display_cones self.sma = lib.common_ta_overlap_model.sma diff --git a/openbb_terminal/core/sdk/trail_map.csv b/openbb_terminal/core/sdk/trail_map.csv index dd76608128d..eb67fd2b412 100644 --- a/openbb_terminal/core/sdk/trail_map.csv +++ b/openbb_terminal/core/sdk/trail_map.csv @@ -540,6 +540,7 @@ ta.bbands,common_ta_volatility_model.bbands,common_ta_volatility_view.display_bb ta.cci,common_ta_momentum_model.cci,common_ta_momentum_view.display_cci ta.cg,common_ta_momentum_model.cg,common_ta_momentum_view.display_cg ta.clenow,common_ta_momentum_model.clenow_momentum,common_ta_momentum_view.display_clenow_momentum +ta.cones,common_ta_volatility_model.cones,common_ta_volatility_view.display_cones ta.demark,common_ta_momentum_model.demark_seq,common_ta_momentum_view.display_demark ta.donchian,common_ta_volatility_model.donchian,common_ta_volatility_view.display_donchian ta.ema,common_ta_overlap_model.ema, @@ -551,6 +552,12 @@ ta.ma,common_ta_overlap_view.view_ma,common_ta_overlap_view.view_ma ta.macd,common_ta_momentum_model.macd,common_ta_momentum_view.display_macd ta.obv,common_ta_volume_model.obv,common_ta_volume_view.display_obv ta.rsi,common_ta_momentum_model.rsi,common_ta_momentum_view.display_rsi +ta.rvol_garman_klass,common_ta_volatility_model.garman_klass, +ta.rvol_hodges_tompkins,common_ta_volatility_model.rvol_hodges_tompkins, +ta.rvol_parkinson,common_ta_volatility_model.rvol_parkinson, +ta.rvol_rogers_satchell,common_ta_volatility_model.rvol_rogers_satchell, +ta.rvol_std,common_ta_volatility_model.rvol_standard_deviation, +ta.rvol_yang_zhang,common_ta_volatility_model.rvol_yang_zhang, ta.sma,common_ta_overlap_model.sma, ta.stoch,common_ta_momentum_model.stoch,common_ta_momentum_view.display_stoch ta.vwap,common_ta_overlap_model.vwap,common_ta_overlap_view.view_vwap