Files
OpenBB/gamestonk_terminal/prediction_techniques/ets.py

515 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import argparse
import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pandas.plotting import register_matplotlib_converters
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from gamestonk_terminal.helper_funcs import (
check_positive,
parse_known_args_and_warn,
valid_date,
patch_pandas_text_adjustment,
get_next_stock_market_days,
plot_autoscale,
)
from gamestonk_terminal.prediction_techniques.pred_helper import (
print_pretty_prediction,
price_prediction_backtesting_color,
print_prediction_kpis,
)
from gamestonk_terminal.config_plot import PLOT_DPI
from gamestonk_terminal import feature_flags as gtff
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(
add_help=False,
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.",
)
parser.add_argument(
"-e",
"--end",
action="store",
type=valid_date,
dest="s_end_date",
default=None,
help="The end date (format YYYY-MM-DD) to select - Backtesting",
)
try:
ns_parser = parse_known_args_and_warn(parser, l_args)
if not ns_parser:
return
# BACKTESTING
if ns_parser.s_end_date:
if ns_parser.s_end_date < df_stock.index[0]:
print(
"Backtesting not allowed, since End Date is older than Start Date of historical data\n"
)
return
if (
ns_parser.s_end_date
< get_next_stock_market_days(
last_stock_day=df_stock.index[0], n_next_days=5 + ns_parser.n_days
)[-1]
):
print(
"Backtesting not allowed, since End Date is too close to Start Date to train model\n"
)
return
future_index = get_next_stock_market_days(
last_stock_day=ns_parser.s_end_date, n_next_days=ns_parser.n_days
)
if future_index[-1] > datetime.datetime.now():
print(
"Backtesting not allowed, since End Date + Prediction days is in the future\n"
)
return
df_future = df_stock[future_index[0] : future_index[-1]]
df_stock = df_stock[: ns_parser.s_end_date]
# Get ETS model
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("\nFit model parameters:")
for key, value in model.params.items():
print(f"{key} {' '*(18-len(key))}: {value}")
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")
# Plotting
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
plt.plot(df_stock.index, df_stock["5. adjusted close"], lw=2)
# BACKTESTING
if ns_parser.s_end_date:
plt.title(f"BACKTESTING: {title} on {s_ticker}")
else:
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",
)
# BACKTESTING
if ns_parser.s_end_date:
plt.plot(
df_future.index,
df_future["5. adjusted close"],
lw=2,
c="tab:blue",
ls="--",
)
plt.plot(
[df_stock.index[-1], df_future.index[0]],
[
df_stock["5. adjusted close"].values[-1],
df_future["5. adjusted close"].values[0],
],
lw=1,
c="tab:blue",
linestyle="--",
)
if gtff.USE_ION:
plt.ion()
plt.show()
# BACKTESTING
if ns_parser.s_end_date:
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
plt.subplot(211)
plt.plot(
df_future.index,
df_future["5. adjusted close"],
lw=2,
c="tab:blue",
ls="--",
)
plt.plot(df_pred.index, df_pred, lw=2, c="green")
plt.scatter(
df_future.index,
df_future["5. adjusted close"],
c="tab:blue",
lw=3,
)
plt.plot(
[df_stock.index[-1], df_future.index[0]],
[
df_stock["5. adjusted close"].values[-1],
df_future["5. adjusted close"].values[0],
],
lw=2,
c="tab:blue",
ls="--",
)
plt.scatter(df_pred.index, df_pred, c="green", lw=3)
plt.plot(
[df_stock.index[-1], df_pred.index[0]],
[df_stock["5. adjusted close"].values[-1], df_pred.values[0]],
lw=2,
c="green",
ls="--",
)
plt.title("BACKTESTING: Real data price versus Prediction")
plt.xlim(
df_stock.index[-1],
df_pred.index[-1] + datetime.timedelta(days=1),
)
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.legend(["Real data", "Prediction data"])
plt.xticks([])
plt.subplot(212)
plt.axhline(y=0, color="k", linestyle="--", linewidth=2)
plt.plot(
df_future.index,
100
* (df_pred.values - df_future["5. adjusted close"].values)
/ df_future["5. adjusted close"].values,
lw=2,
c="red",
)
plt.scatter(
df_future.index,
100
* (df_pred.values - df_future["5. adjusted close"].values)
/ df_future["5. adjusted close"].values,
c="red",
lw=5,
)
plt.title("BACKTESTING: Error between Real data and Prediction [%]")
plt.plot(
[df_stock.index[-1], df_future.index[0]],
[
0,
100
* (
df_pred.values[0]
- df_future["5. adjusted close"].values[0]
)
/ df_future["5. adjusted close"].values[0],
],
lw=2,
ls="--",
c="red",
)
plt.xlim(
df_stock.index[-1],
df_pred.index[-1] + datetime.timedelta(days=1),
)
plt.xlabel("Time")
plt.ylabel("Prediction Error (%)")
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.legend(["Real data", "Prediction data"])
if gtff.USE_ION:
plt.ion()
plt.show()
# Refactor prediction dataframe for backtesting print
df_pred.name = "Prediction"
df_pred = df_pred.to_frame()
df_pred["Real"] = df_future["5. adjusted close"]
if gtff.USE_COLOR:
patch_pandas_text_adjustment()
print("Time Real [$] x Prediction [$]")
print(
df_pred.apply(
price_prediction_backtesting_color, axis=1
).to_string()
)
else:
print(df_pred[["Real", "Prediction"]].round(2).to_string())
print("")
print_prediction_kpis(
df_pred["Real"].values, df_pred["Prediction"].values
)
else:
# 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