Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time-series forecasting (Exponential Smoothing) with Darts Integration #1851

Merged
merged 40 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1e6546a
Working exponential with bugs
martinb-ai May 17, 2022
42f840d
utilizing business datatime + fillers
martinb-ai May 18, 2022
5edcad7
working figures
martinb-ai May 19, 2022
0f0aab4
expo with more functionality
martinb-ai May 19, 2022
09f4ebb
historical forcasting feat.
martinb-ai May 20, 2022
93c3c4f
numeric values for forcasted days
martinb-ai May 20, 2022
b6d7a87
print pretty predictions
martinb-ai May 20, 2022
580cde9
formatting
martinb-ai May 20, 2022
22b0b2a
code cleanup
martinb-ai May 20, 2022
ddc88cf
doc strings fix
martinb-ai May 20, 2022
0ef0ba1
more docs
martinb-ai May 20, 2022
3ebbb2a
quick fix
martinb-ai May 20, 2022
223bcd7
Merge branch 'OpenBB-finance:main' into timeseries-forcasting
martinb-ai May 20, 2022
3493af6
refactoring
martinb-ai May 21, 2022
0fc8e5e
poetry updates
martinb-ai May 21, 2022
d93c717
typo
martinb-ai May 21, 2022
a4f1834
test script update
martinb-ai May 21, 2022
bf25431
update requirements.txt
martinb-ai May 21, 2022
d05625a
refactoring and Hugo website
martinb-ai May 21, 2022
0c2cbdb
Update _index.md
martinb-ai May 21, 2022
cca5efa
reverting poetry and requirements for now.
martinb-ai May 21, 2022
1e73690
Merge branch 'main' into timeseries-forcasting
DidierRLopes May 21, 2022
6beb2b6
minor mod and removal of space
martinb-ai May 21, 2022
5f5c618
Update readme for forecasting
martinb-ai May 21, 2022
0b0f4b9
removal of old TF version
martinb-ai May 21, 2022
50d5f3e
Merge branch 'main' into timeseries-forcasting
jmaslek May 22, 2022
adfaedc
reqs and poetry
martinb-ai May 23, 2022
5e415b3
linting
martinb-ai May 23, 2022
4de697c
merge main.yml from main
martinb-ai May 23, 2022
3f491c3
supress warnings due to lib version change
martinb-ai May 23, 2022
8bcf6b2
reverting main.yml
martinb-ai May 23, 2022
d354f3f
retrying expo on main.yml
martinb-ai May 23, 2022
caa867f
Merge branch 'main' into timeseries-forcasting
martinb-ai May 23, 2022
5ebdcd1
fixing fundamentalanalysis version
martinb-ai May 23, 2022
d9b5bd7
update poetry fundamentalanalysis
martinb-ai May 23, 2022
2372379
type casting arg parse
martinb-ai May 24, 2022
5340c45
seasonal period default update
martinb-ai May 24, 2022
184edd7
Merge branch 'main' into timeseries-forcasting
martinb-ai May 24, 2022
aaf46f6
Merge branch 'main' into timeseries-forcasting
DidierRLopes May 24, 2022
a712011
update en.yml
martinb-ai May 24, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions openbb_terminal/common/prediction_techniques/expo_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
""" Probablistic Exponential Smoothing Model"""
__docformat__ = "numpy"

import logging
from typing import Any, Tuple, Union, List

import numpy as np
import pandas as pd
from darts import TimeSeries
from darts.models import ExponentialSmoothing
from darts.dataprocessing.transformers import MissingValuesFiller
from darts.utils.utils import ModelMode, SeasonalityMode
from darts.metrics import mape

from openbb_terminal.decorators import log_start_end
from openbb_terminal.rich_config import console


TRENDS = ["N", "A", "M"]
SEASONS = ["N", "A", "M"]
PERIODS = [4, 5, 7]
DAMPED = ["T", "F"]

logger = logging.getLogger(__name__)


@log_start_end(log=logger)
def get_expo_data(
data: Union[pd.Series, pd.DataFrame],
trend: str = "A",
seasonal: str = "A",
seasonal_periods: int = None,
damped: str = "F",
n_predict: int = 30,
start_window: float = 0.65,
forecast_horizon: int = 3,
) -> Tuple[List[float], List[float], Any, Any]:

"""Performs Probabalistic Exponential Smoothing forecasting
This is a wrapper around statsmodels Holt-Winters' Exponential Smoothing;
we refer to this link for the original and more complete documentation of the parameters.

https://unit8co.github.io/darts/generated_api/darts.models.forecasting.exponential_smoothing.html?highlight=exponential

Parameters
----------
data : Union[pd.Series, np.ndarray]
Input data.
trend: str
Trend component. One of [N, A, M]
Defaults to ADDITIVE.
seasonal: str
Seasonal component. One of [N, A, M]
Defaults to ADDITIVE.
seasonal_periods: int
Number of seasonal periods in a year
If not set, inferred from frequency of the series.
damped: str
Dampen the function
n_predict: int
Number of days to forecast
start_window: float
Size of sliding window from start of timeseries and onwards
forecast_horizon: int
Number of days to forecast when backtesting and retraining historical

Returns
-------
List[float]
Adjusted Data series
List[float]
List of predicted values
Any
Fit Prob. Expo model object.
"""

filler = MissingValuesFiller()
data["date"] = data.index # add temp column since we need to use index col for date
ticker_series = TimeSeries.from_dataframe(
data,
time_col="date",
value_cols=["AdjClose"],
freq="B",
fill_missing_dates=True,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine to leave this here, but it might incur a small performance penalty. If you happen to already have the guarantee beforehand that your DataFrame does not have missing dates (even if it has missing values), you can consider setting this to False.

)

ticker_series = filler.transform(ticker_series)
ticker_series = ticker_series.astype(np.float32)
train, val = ticker_series.split_before(0.85)

if trend == "M":
trend = ModelMode.MULTIPLICATIVE
elif trend == "N":
trend = ModelMode.NONE
else: # Default
trend = ModelMode.ADDITIVE

if seasonal == "M":
seasonal = SeasonalityMode.MULTIPLICATIVE
elif seasonal == "N":
seasonal = SeasonalityMode.NONE
else: # Default
seasonal = SeasonalityMode.ADDITIVE

damped = True if damped == "T" else False

# Model Init
model_es = ExponentialSmoothing(
trend=trend, seasonal=seasonal, seasonal_periods=seasonal_periods, damped=damped
)

# Training model based on historical backtesting
historical_fcast_es = model_es.historical_forecasts(
ticker_series,
start=start_window,
forecast_horizon=forecast_horizon,
verbose=True,
)

# Show forecast over validation # and then +n_predict afterwards sampled 10 times per point
probabilistic_forecast = model_es.predict(n_predict, num_samples=10)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 samples is somewhat on the low side - something like 500 would seem more reasonable to estimate the distribution. Generating these samples is fast in Numpy so it shouldn't incur any noticeable penalty, and it'll improve precision (especially as later you need the 10th and 90th percentiles).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, will update. Thanks for the insight.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @hrzn ❤️

We are going to revamp our entire prediction menu with Darts and create a release around it 🚀 🚀

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome :)

precision = mape(val, probabilistic_forecast) # mape = mean average precision error
console.print(f"model {model_es} obtains MAPE: {precision:.2f}% \n") # TODO

return (
ticker_series,
historical_fcast_es,
probabilistic_forecast,
precision,
model_es,
)
116 changes: 116 additions & 0 deletions openbb_terminal/common/prediction_techniques/expo_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Probablistic Exponential Smoothing View"""
__docformat__ = "numpy"

import logging
import os
from typing import List, Union

import matplotlib.pyplot as plt
import pandas as pd

from openbb_terminal.config_terminal import theme
from openbb_terminal.common.prediction_techniques import expo_model
from openbb_terminal.config_plot import PLOT_DPI
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import (
export_data,
plot_autoscale,
)
from openbb_terminal.rich_config import console
from openbb_terminal.common.prediction_techniques.pred_helper import (
print_pretty_prediction,
)

logger = logging.getLogger(__name__)

@log_start_end(log=logger)
def display_expo_forecast(
data: Union[pd.DataFrame, pd.Series],
ticker_name: str,
trend: str,
seasonal: str,
seasonal_periods: int,
damped: str,
n_predict: int,
start_window: float,
forecast_horizon: int,
export: str = "",
):
"""Display Probalistic Exponential Smoothing forecast

Parameters
----------
data : Union[pd.Series, np.array]
Data to forecast
trend: str
Trend component. One of [N, A, M]
Defaults to ADDITIVE.
seasonal: str
Seasonal component. One of [N, A, M]
Defaults to ADDITIVE.
seasonal_periods: int
Number of seasonal periods in a year
If not set, inferred from frequency of the series.
damped: str
Dampen the function
n_predict: int
Number of days to forecast
start_window: float
Size of sliding window from start of timeseries and onwards
forecast_horizon: int
Number of days to forecast when backtesting and retraining historical
export: str
Format to export data
external_axes : Optional[List[plt.Axes]], optional
External axes (2 axis is expected in the list), by default None
"""
(
ticker_series,
historical_fcast_es,
predicted_values,
precision,
_,
) = expo_model.get_expo_data(
data,
trend,
seasonal,
seasonal_periods,
damped,
n_predict,
start_window,
forecast_horizon,
)

# Plotting with Matplotlib
external_axes = None
if not external_axes:
fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
else:
if len(external_axes) != 1:
logger.error("Expected list of one axis item.")
console.print("[red]Expected list of one axis item.\n[/red]")
return
(ax,) = external_axes

# ax = fig.get_axes()[0] # fig gives list of axes (only one for this case)
ticker_series.plot(label="Actual AdjClose", figure=fig)
historical_fcast_es.plot(
label="Backtest 3-Days ahead forecast (Exp. Smoothing)", figure=fig
)
predicted_values.plot(
label="Probabilistic Forecast", low_quantile=0.1, high_quantile=0.9, figure=fig
)
ax.set_title(
f"PES for ${ticker_name} for next [{n_predict}] days (Model MAPE={round(precision,2)}%)"
)
ax.set_ylabel("Adj. Closing")
ax.set_xlabel("Date")
theme.style_primary_axis(ax)

if not external_axes:
theme.visualize_output()

numeric_forecast = predicted_values.quantile_df()["AdjClose_0.5"].tail(n_predict)
print_pretty_prediction(numeric_forecast, data["AdjClose"].iloc[-1])

export_data(export, os.path.dirname(os.path.abspath(__file__)), "expo")
4 changes: 4 additions & 0 deletions openbb_terminal/stocks/prediction_techniques/pred_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@
display_mc_forecast as mc,
)

from openbb_terminal.common.prediction_techniques.expo_view import (
display_expo_forecast as expo,
)

# Models
models = _models(os.path.abspath(os.path.dirname(prediction_techniques.__file__)))
106 changes: 105 additions & 1 deletion openbb_terminal/stocks/prediction_techniques/pred_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
neural_networks_view,
pred_helper,
regression_view,
expo_view,
expo_model
)
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import (
Expand Down Expand Up @@ -54,6 +56,7 @@ class PredictionTechniquesController(BaseController):
"lstm",
"conv1d",
"mc",
"expo",
]

PATH = "/stocks/pred/"
Expand Down Expand Up @@ -90,6 +93,10 @@ def __init__(
choices["ets"]["-s"] = {c: {} for c in ets_model.SEASONS}
choices["arima"]["-i"] = {c: {} for c in arima_model.ICS}
choices["mc"]["--dist"] = {c: {} for c in mc_model.DISTRIBUTIONS}
choices["expo"]["-t"] = {c: {} for c in expo_model.TRENDS}
choices["expo"]["-s"] = {c: {} for c in expo_model.SEASONS}
choices["expo"]["-p"] = {c: {} for c in expo_model.PERIODS}
choices["expo"]["-dp"] = {c: {} for c in expo_model.DAMPED}
self.completer = NestedCompleter.from_nested_dict(choices)

def print_help(self):
Expand Down Expand Up @@ -118,7 +125,8 @@ def print_help(self):
rnn Recurrent Neural Network
lstm Long-Short Term Memory
conv1d 1D Convolutional Neural Network
mc Monte-Carlo simulations[/cmds]
mc Monte-Carlo simulations
expo Probablistic Exponential Smoothing[/cmds]
"""
console.print(text=help_text, menu="Stocks - Prediction Techniques")

Expand Down Expand Up @@ -714,3 +722,99 @@ def call_mc(self, other_args: List[str]):
use_log=ns_parser.dist == "lognormal",
export=ns_parser.export,
)

@log_start_end(log=logger)
def call_expo(self, other_args: List[str]):
"""Process expo command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
prog="expo",
description="""
Perform Probablistic Exponential Smoothing forecast
Trend: N: None, A: Additive, M: Multiplicative
Seasonality: N: None, A: Additive, M: Multiplicative
Damped: T: True, F: False
""",
)
parser.add_argument(
"-n",
"--n_days",
action="store",
dest="n_days",
type=check_positive,
default=5,
help="prediction days.",
)
parser.add_argument(
"-t",
"--trend",
action="store",
dest="trend",
choices=expo_model.TRENDS,
default="A",
help="Trend: N: None, A: Additive, M: Multiplicative.",
)
parser.add_argument(
"-s",
"--seasonal",
action="store",
dest="seasonal",
choices=expo_model.SEASONS,
default="A",
help="Seasonality: N: None, A: Additive, M: Multiplicative.",
)
parser.add_argument(
"-p",
"--periods",
action="store",
dest="seasonal_periods",
type=check_positive,
default=5,
help="Seasonal periods: 4: Quarters, 5: Business Days, 7: Weekly",
)
parser.add_argument(
"-d",
"--damped",
action="store",
dest="damped",
default="F",
help="Dampening",
)
parser.add_argument(
"-w",
"--window",
action="store",
dest="start_window",
default=0.65,
help="Start point for rolling training and forecast window. 0.0-1.0",
)
parser.add_argument(
"-f",
"--forecasthorizon",
action="store",
dest="forecast_horizon",
default=3,
help="Days/Points to forecast when training and performing historical back-testing",
)

ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)

if ns_parser:
if self.target != "AdjClose":
console.print("Expo Prediction designed for AdjClose prices\n")

expo_view.display_expo_forecast(
data=self.stock,
ticker_name=self.ticker,
n_predict=ns_parser.n_days,
trend=ns_parser.trend,
seasonal=ns_parser.seasonal,
seasonal_periods=ns_parser.seasonal_periods,
damped = ns_parser.damped,
start_window = ns_parser.start_window,
forecast_horizon = ns_parser.forecast_horizon,
export=ns_parser.export,
)
Loading