From 9bc9f2213d7efefc9e3b594bc2aa1bade8d9b6a5 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 8 Mar 2021 02:07:22 +0000 Subject: [PATCH 1/6] Add several exponential smoothing techniques to prediction menu --- .../prediction_techniques/ets.py | 305 ++++++++++++++++++ .../prediction_techniques/pred_menu.py | 6 + 2 files changed, 311 insertions(+) create mode 100644 gamestonk_terminal/prediction_techniques/ets.py diff --git a/gamestonk_terminal/prediction_techniques/ets.py b/gamestonk_terminal/prediction_techniques/ets.py new file mode 100644 index 00000000000..d0e416a62a5 --- /dev/null +++ b/gamestonk_terminal/prediction_techniques/ets.py @@ -0,0 +1,305 @@ +import argparse +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from pandas.plotting import register_matplotlib_converters +import pmdarima +from statsmodels.tsa.holtwinters import ExponentialSmoothing +from gamestonk_terminal.helper_funcs import ( + check_positive, + get_next_stock_market_days, + parse_known_args_and_warn, + print_pretty_prediction, +) + +register_matplotlib_converters() + +trend = "Ad" # Additive damped, A Additive, N None +seasonal = "N" # None, A Additive, M Multiplicative +seasonal_periods = 5 + + +def check_valid_trend(trend: str) -> str: + if trend == "N" or trend == "A" or trend == "Ad": + return trend + raise argparse.ArgumentTypeError( + "Invalid trend selected. Choose between 'N', 'A', and 'Ad'" + ) + + +def check_valid_seasonal(seasonal: str) -> str: + if seasonal == "N" or seasonal == "A" or seasonal == "M": + return seasonal + raise argparse.ArgumentTypeError( + "Invalid seasonal selected. Choose between 'N', 'A', and 'M'" + ) + + +def exponential_smoothing(l_args, s_ticker, df_stock): + parser = argparse.ArgumentParser( + prog="ets", + description=""" + Exponential Smoothing, see https://otexts.com/fpp2/taxonomy.html + + Trend='N', Seasonal='N': Simple Exponential Smoothing + Trend='N', Seasonal='A': Exponential Smoothing + Trend='N', Seasonal='M': Exponential Smoothing + Trend='A', Seasonal='N': Holt’s linear method + Trend='A', Seasonal='A': Additive Holt-Winters’ method + Trend='A', Seasonal='M': Multiplicative Holt-Winters’ method + Trend='Ad', Seasonal='N': Additive damped trend method + Trend='Ad', Seasonal='A': Exponential Smoothing + Trend='Ad', Seasonal='M': Holt-Winters’ damped method + + Trend component: N: None, A: Additive, Ad: Additive Damped + Seasonality component: N: None, A: Additive, M: Multiplicative + """, + ) + + parser.add_argument( + "-d", + "--days", + action="store", + dest="n_days", + type=check_positive, + default=5, + help="prediction days.", + ) + parser.add_argument( + "-t", + "--trend", + action="store", + dest="trend", + type=check_valid_trend, + default="N", + help="Trend component: N: None, A: Additive, Ad: Additive Damped.", + ) + parser.add_argument( + "-s", + "--seasonal", + action="store", + dest="seasonal", + type=check_valid_seasonal, + default="N", + help="Seasonality component: N: None, A: Additive, M: Multiplicative.", + ) + parser.add_argument( + "-p", + "--periods", + action="store", + dest="seasonal_periods", + type=check_positive, + default=5, + help="Seasonal periods.", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, l_args) + + model, title = get_exponential_smoothing_model( + df_stock["5. adjusted close"].values, + ns_parser.trend, + ns_parser.seasonal, + ns_parser.seasonal_periods, + ) + + if model.mle_retvals.success: + forecast = model.forecast(ns_parser.n_days) + + l_pred_days = get_next_stock_market_days( + last_stock_day=df_stock["5. adjusted close"].index[-1], + n_next_days=ns_parser.n_days, + ) + df_pred = pd.Series(forecast, index=l_pred_days, name="Price") + + if ~np.isnan(forecast).any(): + + print(f"\n{title}") + print(f"\nFit model parameters:") + for key, value in model.params.items(): + print(f"{key} {' '*(18-len(key))}: {value}") + + print(f"\nAssess fit model:") + print(f"AIC: {round(model.aic, 2)}") + print(f"BIC: {round(model.bic, 2)}") + print(f"SSE: {round(model.sse, 2)}\n") + + # Plotting + plt.figure() + plt.plot(df_stock.index, df_stock["5. adjusted close"], lw=2) + plt.title(f"{title} on {s_ticker}") + + plt.xlim( + df_stock.index[0], + get_next_stock_market_days(df_pred.index[-1], 1)[-1], + ) + plt.xlabel("Time") + plt.ylabel("Share Price ($)") + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid( + b=True, which="minor", color="#999999", linestyle="-", alpha=0.2 + ) + plt.plot( + [df_stock.index[-1], df_pred.index[0]], + [df_stock["5. adjusted close"].values[-1], df_pred.values[0]], + lw=1, + c="tab:green", + linestyle="--", + ) + plt.plot(df_pred.index, df_pred, lw=2, c="tab:green") + plt.axvspan( + df_stock.index[-1], + df_pred.index[-1], + facecolor="tab:orange", + alpha=0.2, + ) + _, _, ymin, ymax = plt.axis() + plt.vlines( + df_stock.index[-1], + ymin, + ymax, + linewidth=1, + linestyle="--", + color="k", + ) + plt.ion() + plt.show() + + # Print prediction data + print_pretty_prediction( + df_pred, df_stock["5. adjusted close"].values[-1] + ) + print("") + + else: + print("RuntimeWarning: invalid value encountered in double_scalars.") + else: + print("ConvergenceWarning: Optimization failed to converge.") + + except Exception as e: + print(e) + print("") + + +def get_exponential_smoothing_model(data, trend, seasonal, seasonal_periods): + if trend == "N": # None + if seasonal == "N": # None + title = "Trend='N', Seasonal='N': Simple Exponential Smoothing" + ETS = ExponentialSmoothing( + data, + trend=None, + damped_trend=False, + seasonal=None, + ) + model = ETS.fit( + smoothing_level=None, smoothing_trend=None, damping_trend=None + ) + + elif seasonal == "A": # Additive + title = "Trend='N', Seasonal='A': Exponential Smoothing" + ETS = ExponentialSmoothing( + data, + trend=None, + damped_trend=False, + seasonal="add", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit(smoothing_level=None, smoothing_seasonal=None) + + elif seasonal == "M": # Multiplicative + title = "Trend='N', Seasonal='M': Exponential Smoothing" + ETS = ExponentialSmoothing( + data, + trend=None, + damped_trend=False, + seasonal="mul", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit(smoothing_level=None, smoothing_seasonal=None) + + elif trend == "A": # Additive + if seasonal == "N": # None + title = "Trend='A', Seasonal='N': Holt’s linear method" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=False, + seasonal=None, + ) + model = ETS.fit( + smoothing_level=None, smoothing_trend=None, damping_trend=None + ) + + elif seasonal == "A": # Additive + title = "Trend='A', Seasonal='A': Additive Holt-Winters’ method" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=False, + seasonal="add", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit( + smoothing_level=None, smoothing_trend=None, smoothing_seasonal=None + ) + + elif seasonal == "M": # Multiplicative + title = "Trend='A', Seasonal='M': Multiplicative Holt-Winters’ method" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=False, + seasonal="mul", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit( + smoothing_level=None, smoothing_trend=None, smoothing_seasonal=None + ) + + elif trend == "Ad": # Additive damped + if seasonal == "N": # None + title = "Trend='Ad', Seasonal='N': Additive damped trend method" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=True, + seasonal=None, + ) + model = ETS.fit( + smoothing_level=None, smoothing_trend=None, damping_trend=None + ) + + elif seasonal == "A": # Additive + title = "Trend='Ad', Seasonal='A': Exponential Smoothing" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=True, + seasonal="add", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit( + smoothing_level=None, + smoothing_trend=None, + damping_trend=None, + smoothing_seasonal=None, + ) + + elif seasonal == "M": # Multiplicative + title = "Trend='Ad', Seasonal='M': Holt-Winters’ damped method" + ETS = ExponentialSmoothing( + data, + trend="add", + damped_trend=True, + seasonal="mul", + seasonal_periods=seasonal_periods, + ) + model = ETS.fit( + smoothing_level=None, + smoothing_trend=None, + damping_trend=None, + smoothing_seasonal=None, + ) + + return model, title \ No newline at end of file diff --git a/gamestonk_terminal/prediction_techniques/pred_menu.py b/gamestonk_terminal/prediction_techniques/pred_menu.py index 336570ca73e..ae7d82e38b2 100644 --- a/gamestonk_terminal/prediction_techniques/pred_menu.py +++ b/gamestonk_terminal/prediction_techniques/pred_menu.py @@ -1,6 +1,7 @@ import argparse from gamestonk_terminal.prediction_techniques import sma +from gamestonk_terminal.prediction_techniques import ets from gamestonk_terminal.prediction_techniques import knn from gamestonk_terminal.prediction_techniques import regression from gamestonk_terminal.prediction_techniques import arima @@ -27,6 +28,7 @@ def print_prediction(s_ticker, s_start, s_interval): print(" quit quit to abandon program") print("") print(" sma simple moving average") + print(" ets exponential smoothing (e.g. Holt-Winters)") print(" knn k-Nearest Neighbors") print(" linear linear regression (polynomial 1)") print(" quadratic quadratic regression (polynomial 2)") @@ -51,6 +53,7 @@ def pred_menu(df_stock, s_ticker, s_start, s_interval): "q", "quit", "sma", + "ets", "knn", "linear", "quadratic", @@ -96,6 +99,9 @@ def pred_menu(df_stock, s_ticker, s_start, s_interval): elif ns_known_args.cmd == "sma": sma.simple_moving_average(l_args, s_ticker, df_stock) + elif ns_known_args.cmd == "ets": + ets.exponential_smoothing(l_args, s_ticker, df_stock) + elif ns_known_args.cmd == "knn": knn.k_nearest_neighbors(l_args, s_ticker, df_stock) From 00d818e8d1ff2dd28ece6edbdcfaca8e8b5a93ca Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 8 Mar 2021 02:17:35 +0000 Subject: [PATCH 2/6] Add extra line at bottom juusst for lint --- gamestonk_terminal/prediction_techniques/ets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/prediction_techniques/ets.py b/gamestonk_terminal/prediction_techniques/ets.py index d0e416a62a5..8573c13ff67 100644 --- a/gamestonk_terminal/prediction_techniques/ets.py +++ b/gamestonk_terminal/prediction_techniques/ets.py @@ -302,4 +302,4 @@ def get_exponential_smoothing_model(data, trend, seasonal, seasonal_periods): smoothing_seasonal=None, ) - return model, title \ No newline at end of file + return model, title From a33b1cc8143bd2c3a6638b43642fcfb3a78a289c Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 8 Mar 2021 02:19:10 +0000 Subject: [PATCH 3/6] now its flake8, aww jeez --- gamestonk_terminal/prediction_techniques/ets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/prediction_techniques/ets.py b/gamestonk_terminal/prediction_techniques/ets.py index 8573c13ff67..c19550c8929 100644 --- a/gamestonk_terminal/prediction_techniques/ets.py +++ b/gamestonk_terminal/prediction_techniques/ets.py @@ -115,11 +115,11 @@ def exponential_smoothing(l_args, s_ticker, df_stock): if ~np.isnan(forecast).any(): print(f"\n{title}") - print(f"\nFit model parameters:") + print("\nFit model parameters:") for key, value in model.params.items(): print(f"{key} {' '*(18-len(key))}: {value}") - print(f"\nAssess fit model:") + print("\nAssess fit model:") print(f"AIC: {round(model.aic, 2)}") print(f"BIC: {round(model.bic, 2)}") print(f"SSE: {round(model.sse, 2)}\n") From 3dd78064f413da3879083c23f2a799101a30d53f Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 8 Mar 2021 02:22:48 +0000 Subject: [PATCH 4/6] Remove unused import --- gamestonk_terminal/prediction_techniques/ets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/prediction_techniques/ets.py b/gamestonk_terminal/prediction_techniques/ets.py index c19550c8929..b52314f43ec 100644 --- a/gamestonk_terminal/prediction_techniques/ets.py +++ b/gamestonk_terminal/prediction_techniques/ets.py @@ -3,7 +3,6 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd from pandas.plotting import register_matplotlib_converters -import pmdarima from statsmodels.tsa.holtwinters import ExponentialSmoothing from gamestonk_terminal.helper_funcs import ( check_positive, From 9e5462907916f44e5d946ec3dc2a2447c599b4eb Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 8 Mar 2021 02:28:47 +0000 Subject: [PATCH 5/6] Add ETS to README --- .../prediction_techniques/README.md | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/gamestonk_terminal/prediction_techniques/README.md b/gamestonk_terminal/prediction_techniques/README.md index 209ff96bf41..bb364ade07c 100644 --- a/gamestonk_terminal/prediction_techniques/README.md +++ b/gamestonk_terminal/prediction_techniques/README.md @@ -4,6 +4,8 @@ This menu aims to predict the share price of a pre-loaded stock, and the usage o * [sma](#sma) - simple moving average + * [ets](#ets) + - Exponential Smoothing (e.g. Holt-Winters) * [knn](#knn) - k-Nearest Neighbors * [linear](#linear) @@ -38,6 +40,17 @@ Simple Moving Average: ![sma](https://user-images.githubusercontent.com/25267873/108604945-d29aea80-73a8-11eb-8dac-6a545b9c52b9.png) +## ets +``` +usage: ets [-t TREND] [-s SEASONAL] [-p SEASONAL_PERIODS] [-d N_DAYS] +``` +Exponential Smoothing: + * -t : trend component: N: None, A: Additive, Ad: Additive Damped. Default N. + * -s : seasonality component: N: None, A: Additive, M: Multiplicative. Default N. + * -p : seasonal periods. Default 5. + * -d : prediciton days. Default 5. +Based on trend+seasonality, the following methods are implemented: https://otexts.com/fpp2/taxonomy.html + ## knn ``` usage: knn [-i N_INPUTS] [-d N_DAYS] [-j N_JUMPS] [-n N_NEIGHBORS] @@ -125,7 +138,7 @@ Facebook's Prophet: ## mlp ``` -usage: mlp [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] +usage: mlp [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] [-o {adam,adagrad,adadelta,adamax,ftrl,nadam,optimizer,rmsprop,sgd}] [-l {mae,mape,mse,msle}] ``` MulitLayer Perceptron: @@ -140,15 +153,15 @@ MulitLayer Perceptron: Due to the complexity of defining a model through command line, one can define it in: [config_neural_network_models.txt](/config_neural_network_models.py) ``` MultiLayer_Perceptron \ - = [ {'Dense': + = [ {'Dense': {'units':50, 'activation':'relu'} }, - {'Dense': + {'Dense': {'units':100, 'activation':'relu'} }, - {'Dense': + {'Dense': {'units':80, 'activation':'relu'} }, - {'Dense': + {'Dense': {'units':30, 'activation':'relu'} }, - {'Dense': + {'Dense': {'activation':'linear'} }] ``` @@ -156,7 +169,7 @@ MultiLayer_Perceptron \ ## rnn ``` -usage: rnn [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] +usage: rnn [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] [-o {adam,adagrad,adadelta,adamax,ftrl,nadam,optimizer,rmsprop,sgd}] [-l {mae,mape,mse,msle}] ``` Recurrent Neural Network: @@ -171,15 +184,15 @@ Recurrent Neural Network: Due to the complexity of defining a model through command line, one can define it in: [config_neural_network_models.txt](/config_neural_network_models.py) ``` Recurrent_Neural_Network \ - = [ {'SimpleRNN': + = [ {'SimpleRNN': {'units':100, 'activation':'linear', 'return_sequences':True} }, - {'SimpleRNN': + {'SimpleRNN': {'units':50, 'activation':'linear', 'return_sequences':True} }, - {'Dropout': + {'Dropout': {'rate':0.2} }, - {'SimpleRNN': + {'SimpleRNN': {'units':21, 'activation':'linear', 'return_sequences':False} }, - {'Dense': + {'Dense': {'activation':'linear'} }] ``` @@ -187,7 +200,7 @@ Recurrent_Neural_Network \ ## lstm ``` -usage: lstm [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] +usage: lstm [-d N_DAYS] [-i N_INPUTS] [-j N_JUMPS] [-e N_EPOCHS] [-p {normalization,standardization,none}] [-o {adam,adagrad,adadelta,adamax,ftrl,nadam,optimizer,rmsprop,sgd}] [-l {mae,mape,mse,msle}] ``` Long-Short Term Memory: @@ -202,11 +215,11 @@ Long-Short Term Memory: Due to the complexity of defining a model through command line, one can define it in: [config_neural_network_models.txt](/config_neural_network_models.py) ``` Long_Short_Term_Memory \ - = [ {'LSTM': + = [ {'LSTM': {'units':25, 'activation':'tanh', 'return_sequences':True} }, - {'LSTM': + {'LSTM': {'units':15, 'activation':'tanh', 'return_sequences':False} }, - {'Dense': + {'Dense': {'activation':'linear'} }] ``` From 0a7978867f37fd56df7ea67c4bf9601ab1e26b97 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 8 Mar 2021 02:33:48 +0000 Subject: [PATCH 6/6] Update README.md --- gamestonk_terminal/prediction_techniques/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/prediction_techniques/README.md b/gamestonk_terminal/prediction_techniques/README.md index bb364ade07c..abf2bde299d 100644 --- a/gamestonk_terminal/prediction_techniques/README.md +++ b/gamestonk_terminal/prediction_techniques/README.md @@ -44,12 +44,13 @@ Simple Moving Average: ``` usage: ets [-t TREND] [-s SEASONAL] [-p SEASONAL_PERIODS] [-d N_DAYS] ``` -Exponential Smoothing: +Exponential Smoothing (based on trend+seasonality, see https://otexts.com/fpp2/taxonomy.html): * -t : trend component: N: None, A: Additive, Ad: Additive Damped. Default N. * -s : seasonality component: N: None, A: Additive, M: Multiplicative. Default N. * -p : seasonal periods. Default 5. * -d : prediciton days. Default 5. -Based on trend+seasonality, the following methods are implemented: https://otexts.com/fpp2/taxonomy.html + +![ets_pltr](https://user-images.githubusercontent.com/25267873/110266847-97a6d280-7fb6-11eb-997e-0b598abc713b.png) ## knn ```