# pylint: disable=too-many-arguments,E1101 """Automatic Statistical Forecast""" __docformat__ = "numpy" import logging import warnings from typing import List, Optional, Tuple, Union import pandas as pd from darts import TimeSeries from statsforecast.core import StatsForecast from openbb_terminal.core.session.current_user import get_current_user from openbb_terminal.decorators import log_start_end from openbb_terminal.forecast import helpers from openbb_terminal.rich_config import USE_COLOR, console warnings.simplefilter("ignore") logger = logging.getLogger(__name__) # pylint: disable=E1123,E1137 def precision_format(best_model: str, index: str, val: float) -> Union[str, float]: if ( index == best_model and USE_COLOR and not get_current_user().preferences.USE_INTERACTIVE_DF ): return f"[#00AAFF]{val:.2f}% [/#00AAFF]" return val @log_start_end(log=logger) def get_autoselect_data( data: Union[pd.Series, pd.DataFrame], target_column: str = "close", seasonal_periods: int = 7, n_predict: int = 5, start_window: float = 0.85, forecast_horizon: int = 5, ) -> Tuple[ Optional[List[type[TimeSeries]]], Optional[List[type[TimeSeries]]], Optional[List[type[TimeSeries]]], Optional[float], Optional[StatsForecast], Optional[Union[int, str]], ]: """Performs Automatic Statistical forecasting This is a wrapper around StatsForecast models; we refer to this link for the original and more complete documentation of the parameters. https://nixtla.github.io/statsforecast/models.html Parameters ---------- data : Union[pd.Series, np.ndarray] Input data. target_column: Optional[str]: Target column to forecast. Defaults to "close". seasonal_periods: int Number of seasonal periods in a year (7 for daily data) If not set, inferred from frequency of the series. 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 ------- Tuple[List[TimeSeries], List[TimeSeries], List[TimeSeries], Optional[float], StatsForecast, Union[int, str]] list[np.ndarray] - Adjusted Data series list[np.ndarray] - List of historical fcast values list[np.ndarray] - List of predicted fcast values Optional[float] - precision StatsForecast - Fit ETS model object. Union[int, str] - Best model """ use_scalers = False # statsforecast preprocessing # when including more time series # the preprocessing is similar _, ticker_series = helpers.get_series(data, target_column, is_scaler=use_scalers) freq = ticker_series.freq_str ticker_series = ticker_series.pd_dataframe().reset_index() ticker_series.columns = ["ds", "y"] ticker_series.insert(0, "unique_id", target_column) # check statsforecast dependency try: from statsforecast.models import ( # pylint: disable=import-outside-toplevel ETS, MSTL, AutoARIMA, AutoCES, Naive, RandomWalkWithDrift, SeasonalNaive, SeasonalWindowAverage, ) except Exception as e: error = str(e) if "cannot import name" in error: console.print( "[red]Please update statsforecast to version 1.2.0 or higher.[/red]" ) else: console.print(f"[red]{error}[/red]") return [], [], [], None, None, None try: # Model Init season_length = int(seasonal_periods) models = [ AutoARIMA(season_length=season_length), ETS(season_length=season_length), AutoCES(season_length=season_length), MSTL(season_length=season_length), SeasonalNaive(season_length=season_length), SeasonalWindowAverage( season_length=season_length, window_size=season_length ), RandomWalkWithDrift(), ] fcst = StatsForecast( df=ticker_series, models=models, freq=freq, verbose=True, fallback_model=Naive(), ) except Exception as e: # noqa error = str(e) if "got an unexpected keyword argument" in error: console.print( "[red]Please update statsforecast to version 1.1.3 or higher.[/red]" ) else: console.print(f"[red]{error}[/red]") return [], [], [], None, None, None # Historical backtesting last_training_point = int((len(ticker_series) - 1) * start_window) historical_fcast = fcst.cross_validation( h=int(forecast_horizon), test_size=len(ticker_series) - last_training_point, n_windows=None, input_size=min(10 * forecast_horizon, len(ticker_series)), ) # change name to AutoETS and AutoCES cols = [ c if (c not in ["ETS", "CES"]) else f"Auto{c}" for c in historical_fcast.columns ] historical_fcast.columns = cols # train new model on entire timeseries to provide best current forecast # we have the historical fcast, now lets predict. forecast = fcst.forecast(int(n_predict)) # change name to AutoETS and AutoCES cols = [c if (c not in ["ETS", "CES"]) else f"Auto{c}" for c in forecast.columns] forecast.columns = cols # Backtesting evaluation y_true = historical_fcast["y"].values model_names = historical_fcast.drop(columns=["ds", "cutoff", "y"]).columns precision_per_model = [ helpers.mean_absolute_percentage_error(y_true, historical_fcast[model].values) for model in model_names ] precision: pd.DataFrame = pd.DataFrame( {"precision": precision_per_model}, index=model_names ) precision = precision.sort_values(by="precision") # select best model best_precision: float = precision["precision"].min() best_model = precision["precision"].idxmin() # print results precision["precision"] = [ # pylint: disable=unsupported-assignment-operation precision_format(best_model, index, val) for index, val in precision["precision"].iteritems() ] console.print("\n") title = ( f"[#00AAFF]{best_model}[/#00AAFF]" if not get_current_user().preferences.USE_INTERACTIVE_DF else best_model ) helpers.print_rich_table( precision, show_index=True, index_name="Model", headers=["MAPE"], title=f"Performance per model.\nBest model: {title}", ) # transform outputs to make them compatible with # plots use_scalers = False _, ticker_series = helpers.get_series( ticker_series.rename(columns={"y": target_column}), target_column, is_scaler=use_scalers, time_col="ds", ) _, forecast = helpers.get_series( forecast.rename(columns={best_model: target_column}), target_column, is_scaler=use_scalers, time_col="ds", ) _, historical_fcast = helpers.get_series( historical_fcast.groupby("ds") .head(1) .rename(columns={best_model: target_column}), target_column, is_scaler=use_scalers, time_col="ds", ) return (ticker_series, historical_fcast, forecast, best_precision, fcst, best_model)