diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a1a62fd675..17f52d31bea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - run: pip install bandit black codespell flake8 mypy pyupgrade safety pylint - run: bandit -r . || true - run: black --check . - - run: codespell --ignore-words-list="mape","ba" --quiet-level=2 --skip=".git" --skip="*.yaml" --skip="*.json" + - run: codespell --ignore-words-list="mape","ba","hist" --quiet-level=2 --skip=".git" --skip="*.yaml" --skip="*.json" - run: flake8 . --count --ignore=E203,W503 --max-line-length=122 --show-source --statistics - run: mypy --ignore-missing-imports . - run: shopt -s globstar && pyupgrade --py36-plus **/*.py diff --git a/ROADMAP.md b/ROADMAP.md index d6ed49eae3d..7cf2d4656f8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -139,8 +139,8 @@ ### Short term -* [x] Summary statistics, cdf, histogram, box-whiskers, cusum, decompose (@didier) - [PR #289](https://github.com/DidierRLopes/GamestonkTerminal/pull/289) -* [ ] Residuals sub-menu with Hypothesis tests (e.g. Kurtosis, Jarques-Bera, ARCH, ADF, ... and such) (@didier) +* [x] Summary stats, CDF, Histogram, Box Whiskers, Decompose (in Trend, Cyclic Trend, Season), CUSUM (@didier) - [PR #289](https://github.com/DidierRLopes/GamestonkTerminal/pull/289) +* [ ] Hypothesis tests (e.g. Kurtosis, Jarques-Bera, ARCH, ADF, ...) (@didier) ### Long term diff --git a/gamestonk_terminal/exploratory_data_analysis/README.md b/gamestonk_terminal/exploratory_data_analysis/README.md new file mode 100644 index 00000000000..244d1079d4b --- /dev/null +++ b/gamestonk_terminal/exploratory_data_analysis/README.md @@ -0,0 +1,136 @@ +# EXPLORATORY DATA ANALYSIS + +This menu aims to perform exploratory data analysis on a pre-loaded stock, and the usage of the following commands along with an example will be exploited below. + +* [summary](#summary) + * brief summary statistics +* [hist](#hist) + * histogram with density plot +* [cdf](#cdf) + * cumulative distribution function +* [bwy](#bwy) + * box and whisker yearly plot +* [bwm](#bwm) + * box and whisker monthly plot +* [rolling](#rolling) + * rolling mean and std deviation +* [decompose](#decompose) + * decomposition in cyclic-trend, season, and residuald +* [cusum](#cusum) + * detects abrupt changes using cumulative sum algorithm +* [acf](#acf) + * (partial) auto-correlation function differentials + + +## summary + +```text +usage: summary +``` + +Summary statistics + +summary + + +## hist + +```text +usage: hist +``` + +Plot histogram and density + +![hist](https://user-images.githubusercontent.com/25267873/112729911-75a1cf80-8f26-11eb-925c-252a90657128.png) + + +## cdf + +```text +usage: cdf +``` + +Plot cumulative distribution function + +![cdf](https://user-images.githubusercontent.com/25267873/112729910-75a1cf80-8f26-11eb-801a-2b44b193022b.png) + + +## bwy + +```text +usage: bwy +``` + +Box and Whisker plot yearly + +![bwy](https://user-images.githubusercontent.com/25267873/112729912-763a6600-8f26-11eb-81b6-3df0b632b3af.png) + + +## bwm + +```text +usage: bwm +``` + +Box and Whisker plot monthly + +![bwm](https://user-images.githubusercontent.com/25267873/112729913-76d2fc80-8f26-11eb-8338-448147ed5703.png) + + +## rolling + +```text +usage: rolling [-w ROLLING_WINDOW] +``` + +Rolling mean and std deviation + +* -w : rolling window. Default 100. + +![rolling](https://user-images.githubusercontent.com/25267873/112729908-75093900-8f26-11eb-9056-16bac2f54386.png) + + +## decompose + +```text +usage: decompose [-m] +``` + +Decompose time series as: + - Additive Time Series = Level + CyclicTrend + Residual + Seasonality + - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality + +* -m : multiplicative model. Default additive. + +![decompose](https://user-images.githubusercontent.com/25267873/112729282-4c337480-8f23-11eb-913c-f30e5c0ef459.png) + +Captura de ecrã 2021-03-27, às 17 32 05 + + +## cusum + +```text +usage: cusum [-t THRESHOLD] [-d DRIFT] +``` + +Cumulative sum algorithm (CUSUM) to detect abrupt changes in data + +* -t : threshold. Default (MAX-min)/40. +* -d : drift. Default (MAX-min)/80. + +![cusum](https://user-images.githubusercontent.com/25267873/112729206-ef37be80-8f22-11eb-9a53-8e8e55c4caf0.png) + + +## acf + +```text +usage: acf [-l LAGS] +``` + +Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data + +* -l : maximum lags to display in plots. Default 40. + +![acf](https://user-images.githubusercontent.com/25267873/112733336-637c5d00-8f37-11eb-9aae-38e75dff9ad2.png) + + diff --git a/gamestonk_terminal/exploratory_data_analysis/eda_api.py b/gamestonk_terminal/exploratory_data_analysis/eda_api.py new file mode 100644 index 00000000000..b00784c64af --- /dev/null +++ b/gamestonk_terminal/exploratory_data_analysis/eda_api.py @@ -0,0 +1,709 @@ +""" Exploratory Data Analysis API """ +__docformat__ = "numpy" + +import argparse +from typing import List +from detecta import detect_cusum +from matplotlib import pyplot as plt +from datetime import datetime +import pandas as pd +from pandas.plotting import register_matplotlib_converters +import numpy as np +import seaborn as sns +import statsmodels.api as sm +import matplotlib.gridspec as gridspec +from statsmodels.tsa.seasonal import seasonal_decompose +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, + check_positive, +) +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff + + +register_matplotlib_converters() + + +def summary(other_args: List[str], stock: pd.DataFrame): + """Print summary statistics + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="summary", + description=""" + Summary statistics + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_stats = stock.describe(percentiles=[0.1, 0.25, 0.5, 0.75, 0.9]) + df_stats.loc["var"] = df_stats.loc["std"] ** 2 + print(df_stats.round(2).to_string()) + + print("") + + except Exception as e: + print(e, "\n") + return + + +def hist(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime): + """Plot histogram and density + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="hist", + description=""" + Histogram with depicted density and rug + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + stock = stock["5. adjusted close"] + + sns.distplot( + stock, + bins=35, + color="blue", + hist=True, + hist_kws={"edgecolor": "black"}, + kde=True, + kde_kws={"color": "black", "lw": 3, "label": "KDE"}, + rug=True, + rug_kws={"edgecolor": "orange"}, + ) + plt.title( + f"Histogram with Density of {ticker} from {start.strftime('%Y-%m-%d')}" + ) + plt.ylabel("Density") + plt.xlabel("Share Price") + plt.grid(True) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def cdf(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime): + """Plot cumulative distribution function + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="cdf", + description=""" + Cumulative distribution function + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + stock = stock["5. adjusted close"] + + cdf = stock.value_counts().sort_index().div(len(stock)).cumsum() + cdf.plot(lw=2) + plt.title( + f"Cumulative Distribution Function of {ticker} from {start.strftime('%Y-%m-%d')}" + ) + plt.ylabel("Probability") + plt.xlabel("Share Price") + minVal = stock.values.min() + q25 = np.quantile(stock.values, 0.25) + medianVal = np.quantile(stock.values, 0.5) + q75 = np.quantile(stock.values, 0.75) + data = [ + (minVal, q25), + (0.25, 0.25), + "r", + (q25, q25), + (0, 0.25), + "r", + (minVal, medianVal), + (0.5, 0.5), + "r", + (medianVal, medianVal), + (0, 0.5), + "r", + (minVal, q75), + (0.75, 0.75), + "r", + (q75, q75), + (0, 0.75), + "r", + ] + plt.plot(*data, ls="--") + plt.text(minVal + (q25 - minVal) / 2, 0.27, "Q1", color="r", fontweight="bold") + plt.text( + minVal + (medianVal - minVal) / 2, + 0.52, + "Median", + color="r", + fontweight="bold", + ) + plt.text(minVal + (q75 - minVal) / 2, 0.77, "Q3", color="r", fontweight="bold") + plt.xlim(cdf.index[0], cdf.index[-1]) + plt.grid(True) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def bwy(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime): + """Box and Whisker plot yearly + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="bwy", + description=""" + Box and Whisker plot yearly + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + stock = stock["5. adjusted close"] + + sns.set(style="whitegrid") + box_plot = sns.boxplot(x=stock.index.year, y=stock) + box_plot.set( + xlabel="Year", + ylabel="Share Price", + title=f"Box-plot per Year of {ticker} from {start.strftime('%Y-%m-%d')}", + ) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def bwm(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime): + """Box and Whisker plot monthly + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="bwm", + description=""" + Box and Whisker plot monthly + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + stock = stock["5. adjusted close"] + + sns.set(style="whitegrid") + box_plot = sns.boxplot(x=stock.index.month, y=stock) + box_plot.set( + xlabel="Month", + ylabel="Share Price", + title=f"Box-plot per Month of {ticker} from {start.strftime('%Y-%m-%d')}", + ) + l_months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] + l_ticks = list() + for val in box_plot.get_xticklabels(): + l_ticks.append(l_months[int(val.get_text()) - 1]) + box_plot.set_xticklabels(l_ticks) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def rolling(other_args: List[str], ticker: str, stock: pd.DataFrame): + """Rolling mean and std deviation + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="rolling", + description=""" + Rolling mean and std deviation + """, + ) + parser.add_argument( + "-w", + "--window", + dest="rolling_window", + type=check_positive, + default=100, + help="rolling window", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + stock = stock["5. adjusted close"] + + rolling_mean = stock.rolling( + ns_parser.rolling_window, center=True, min_periods=1 + ).mean() + rolling_std = stock.rolling( + ns_parser.rolling_window, center=True, min_periods=1 + ).std() + + fig, axMean = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + + axMean.plot(stock.index, stock.values, label=ticker, linewidth=2, color="black") + axMean.plot(rolling_mean, linestyle="--", linewidth=3, color="blue") + axMean.set_xlabel("Time") + axMean.set_ylabel("Share Price", color="blue") + axMean.legend(["Real values", "Rolling Mean"], loc=2) + axMean.tick_params(axis="y", labelcolor="blue") + axStd = axMean.twinx() + axStd.plot( + rolling_std, label="Rolling std", linestyle="--", color="green", linewidth=3 + ) + axStd.set_ylabel("Std Deviation") + axStd.legend(["Rolling std"], loc=1) + axStd.set_ylabel("Share Price standard deviation", color="green") + axStd.tick_params(axis="y", labelcolor="green") + axMean.set_title( + "Rolling mean and std with window " + + str(ns_parser.rolling_window) + + " applied to " + + ticker + ) + plt.xlim([stock.index[0], stock.index[-1]]) + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def decompose(other_args: List[str], ticker: str, stock: pd.DataFrame): + """Decompose time series as: + - Additive Time Series = Level + CyclicTrend + Residual + Seasonality + - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="decompose", + description=""" + Decompose time series as: + - Additive Time Series = Level + CyclicTrend + Residual + Seasonality + - Multiplicative Time Series = Level * CyclicTrend * Residual * Seasonality + """, + ) + parser.add_argument( + "-m", + "--multiplicative", + action="store_true", + default=False, + dest="multiplicative", + help="decompose using multiplicative model instead of additive", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + stock = stock["5. adjusted close"] + + seasonal_periods = 5 + # Hodrick-Prescott filter + # See Ravn and Uhlig: http://home.uchicago.edu/~huhlig/papers/uhlig.ravn.res.2002.pdf + lamb = 107360000000 + + fig = plt.figure( + figsize=plot_autoscale(), dpi=PLOT_DPI, constrained_layout=True + ) + spec = gridspec.GridSpec(ncols=4, nrows=5, figure=fig) + + fig.add_subplot(spec[0, :]) + plt.plot(stock) + + plt.title(ticker + " (Time-Series)") + + if ns_parser.multiplicative: + resultMul = seasonal_decompose( + stock, model="multiplicative", period=seasonal_periods + ) + cycleMul, trendMul = sm.tsa.filters.hpfilter( + resultMul.trend[resultMul.trend.notna().values], lamb=lamb + ) + + # Multiplicative model + fig.add_subplot(spec[1, :4]) + plt.plot(resultMul.trend, lw=2, c="purple") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Multiplicative Cyclic-Trend") + + fig.add_subplot(spec[2, 0:2]) + plt.plot(trendMul, lw=2, c="tab:blue") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Multiplicative Trend component") + + fig.add_subplot(spec[2, 2:]) + plt.plot(cycleMul, lw=2, c="green") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Multiplicative Cycle component") + + fig.add_subplot(spec[3, :]) + plt.plot(resultMul.seasonal, lw=2, c="orange") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Multiplicative Seasonal effect") + + fig.add_subplot(spec[4, :]) + plt.plot(resultMul.resid, lw=2, c="red") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Multiplicative Residuals") + + else: + resultAdd = seasonal_decompose( + stock, model="additive", period=seasonal_periods + ) + cycleAdd, trendAdd = sm.tsa.filters.hpfilter( + resultAdd.trend[resultAdd.trend.notna().values], lamb=lamb + ) + + # Additive model + fig.add_subplot(spec[1, :4]) + plt.plot(resultAdd.trend, lw=2, c="purple") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Additive Cyclic-Trend") + + fig.add_subplot(spec[2, 0:2]) + plt.plot(trendAdd, lw=2, c="tab:blue") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Additive Trend component") + + fig.add_subplot(spec[2, 2:]) + plt.plot(cycleAdd, lw=2, c="green") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Additive Cycle component") + + fig.add_subplot(spec[3, :]) + plt.plot(resultAdd.seasonal, lw=2, c="orange") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Additive Seasonal effect") + + fig.add_subplot(spec[4, :]) + plt.plot(resultAdd.resid, lw=2, c="red") + plt.xlim([stock.index[0], stock.index[-1]]) + plt.title("Additive Residuals") + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + # From # https://otexts.com/fpp2/seasonal-strength.html + + print("Time-Series Level is " + str(round(stock.mean(), 2))) + + if ns_parser.multiplicative: + FtMul = max(0, 1 - np.var(resultMul.resid)) / np.var( + resultMul.trend + resultMul.resid + ) + print("Strength of Trend: %.4f" % FtMul) + FsMul = max( + 0, + 1 + - np.var(resultMul.resid) + / np.var(resultMul.seasonal + resultMul.resid), + ) + print("Strength of Seasonality: %.4f" % FsMul) + + else: + FtAdd = max( + 0, + 1 - np.var(resultAdd.resid) / np.var(resultAdd.trend + resultAdd.resid), + ) + print("Strength of Trend: %.4f" % FtAdd) + FsAdd = max( + 0, + 1 + - np.var(resultAdd.resid) + / np.var(resultAdd.seasonal + resultAdd.resid), + ) + print("Strength of Seasonality: %.4f" % FsAdd) + print("") + + except Exception as e: + print(e, "\n") + return + + +def cusum(other_args: List[str], ticker: str, stock: pd.DataFrame): + """Cumulative sum algorithm (CUSUM) to detect abrupt changes in data + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="cusum", + description=""" + Cumulative sum algorithm (CUSUM) to detect abrupt changes in data + """, + ) + parser.add_argument( + "-t", + "--threshold", + dest="threshold", + type=float, + default=( + max(stock["5. adjusted close"].values) + - min(stock["5. adjusted close"].values) + ) + / 40, + help="threshold", + ) + parser.add_argument( + "-d", + "--drift", + dest="drift", + type=float, + default=( + max(stock["5. adjusted close"].values) + - min(stock["5. adjusted close"].values) + ) + / 80, + help="drift", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + stock = stock["5. adjusted close"] + + detect_cusum(stock.values, ns_parser.threshold, ns_parser.drift, True, True) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return + + +def acf(other_args: List[str], ticker: str, stock: pd.DataFrame, start: datetime): + """Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data + + Parameters + ---------- + other_args : str + Command line arguments to be processed with argparse + ticker : str + Ticker of the stock + stock : pd.DataFrame + Stock data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="acf", + description=""" + Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data + """, + ) + parser.add_argument( + "-l", + "--lags", + dest="lags", + type=check_positive, + default=40, + help="maximum lags to display in plots", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + stock = stock["5. adjusted close"] + + fig = plt.figure( + figsize=plot_autoscale(), dpi=PLOT_DPI, constrained_layout=True + ) + spec = gridspec.GridSpec(ncols=2, nrows=2, figure=fig) + + # Diff Auto-correlation function for original time series + ax_acf = fig.add_subplot(spec[0, 0]) + sm.graphics.tsa.plot_acf( + np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_acf + ) + plt.title(f"Diff({ticker}) Auto-Correlation from {start.strftime('%Y-%m-%d')}") + # Diff Partial auto-correlation function for original time series + ax_pacf = fig.add_subplot(spec[0, 1]) + sm.graphics.tsa.plot_pacf( + np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_pacf + ) + plt.title( + f"Diff({ticker}) Partial Auto-Correlation from {start.strftime('%Y-%m-%d')}" + ) + + # Diff Diff Auto-correlation function for original time series + ax_acf = fig.add_subplot(spec[1, 0]) + sm.graphics.tsa.plot_acf( + np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_acf + ) + plt.title( + f"Diff(Diff({ticker})) Auto-Correlation from {start.strftime('%Y-%m-%d')}" + ) + # Diff Diff Partial auto-correlation function for original time series + ax_pacf = fig.add_subplot(spec[1, 1]) + sm.graphics.tsa.plot_pacf( + np.diff(np.diff(stock.values)), lags=ns_parser.lags, ax=ax_pacf + ) + plt.title( + f"Diff(Diff({ticker})) Partial Auto-Correlation from {start.strftime('%Y-%m-%d')}" + ) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e, "\n") + return diff --git a/gamestonk_terminal/exploratory_data_analysis/eda_controller.py b/gamestonk_terminal/exploratory_data_analysis/eda_controller.py new file mode 100644 index 00000000000..7d3189d1fe6 --- /dev/null +++ b/gamestonk_terminal/exploratory_data_analysis/eda_controller.py @@ -0,0 +1,177 @@ +"""Exploratory Data Analysis Controller Module""" +__docformat__ = "numpy" + +import argparse +from typing import List +from datetime import datetime +import pandas as pd +from matplotlib import pyplot as plt +from gamestonk_terminal.exploratory_data_analysis import eda_api +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import get_flair +from gamestonk_terminal.menu import session +from prompt_toolkit.completion import NestedCompleter + + +class EdaController: + """EDA Controller class""" + + # Command choices + CHOICES = [ + "help", + "q", + "quit", + "summary", + "hist", + "cdf", + "bwy", + "bwm", + "rolling", + "decompose", + "cusum", + "acf", + ] + + def __init__( + self, + stock: pd.DataFrame, + ticker: str, + start: datetime, + interval: str, + ): + """Constructor""" + self.stock = stock + self.ticker = ticker + self.start = start + self.interval = interval + self.stats_parser = argparse.ArgumentParser(add_help=False, prog="stats") + self.stats_parser.add_argument( + "cmd", + choices=self.CHOICES, + ) + + @staticmethod + def print_help(self): + """Print help""" + + s_intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"] + + if self.start: + print( + f"\n{s_intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})" + ) + else: + print(f"\n{s_intraday} Stock: {self.ticker}") + + print("\nExploratory Data Analysis:") + print(" help show this comparison analysis menu again") + print(" q quit this menu, and shows back to main menu") + print(" quit quit to abandon program") + print("") + print(" summary brief summary statistics") + print(" hist histogram with density plot") + print(" cdf cumulative distribution function") + print(" bwy box and whisker yearly plot") + print(" bwm box and whisker monthly plot") + print(" rolling rolling mean and std deviation") + print(" decompose decomposition in cyclic-trend, season, and residuals") + print(" cusum detects abrupt changes using cumulative sum algorithm") + print(" acf (partial) auto-correlation function differentials") + print("") + return + + def switch(self, an_input: str): + """Process and dispatch input + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + (known_args, other_args) = self.stats_parser.parse_known_args(an_input.split()) + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help(self) + + def call_q(self, _): + """Process Q command - quit the menu""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program""" + return True + + def call_summary(self, other_args: List[str]): + """Process summary command""" + eda_api.summary(other_args, self.stock) + + def call_hist(self, other_args: List[str]): + """Process hist command""" + eda_api.hist(other_args, self.ticker, self.stock, self.start) + + def call_cdf(self, other_args: List[str]): + """Process cdf command""" + eda_api.cdf(other_args, self.ticker, self.stock, self.start) + + def call_bwy(self, other_args: List[str]): + """Process bwy command""" + eda_api.bwy(other_args, self.ticker, self.stock, self.start) + + def call_bwm(self, other_args: List[str]): + """Process bwm command""" + eda_api.bwm(other_args, self.ticker, self.stock, self.start) + + def call_rolling(self, other_args: List[str]): + """Process rolling command""" + eda_api.rolling(other_args, self.ticker, self.stock) + + def call_decompose(self, other_args: List[str]): + """Process decompose command""" + eda_api.decompose(other_args, self.ticker, self.stock) + + def call_cusum(self, other_args: List[str]): + """Process cusum command""" + eda_api.cusum(other_args, self.ticker, self.stock) + + def call_acf(self, other_args: List[str]): + """Process acf command""" + eda_api.acf(other_args, self.ticker, self.stock, self.start) + + +def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): + """Statistics Menu""" + + eda_controller = EdaController(stock, ticker, start, interval) + eda_controller.call_help(None) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in eda_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (eda)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (eda)> ") + + try: + plt.close("all") + + process_input = eda_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index c5d95f067f3..cabd406e89c 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -75,6 +75,9 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): print( " dd in-depth due-diligence, \t e.g.: news, analyst, shorts, insider, sec" ) + print( + " eda exploratory data analysis,\t e.g.: decompose, cusum, residuals analysis" + ) print( " pred prediction techniques, \t e.g.: regression, arima, rnn, lstm, prophet" ) diff --git a/poetry.lock b/poetry.lock index cc79aeb62c3..98642b77bdf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -436,6 +436,14 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +[[package]] +name = "detecta" +version = "0.0.5" +description = "Detect events in data" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "entrypoints" version = "0.3" @@ -2778,7 +2786,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "b05035fef8aa3a5dacf6c4713579493f1104296ea25305049466fc47e0c35401" +content-hash = "d0abec748e4bb0fcc0bb22409e4bfdcb6802f2ca9b1849a77941d5a4a954ccc2" [metadata.files] absl-py = [ @@ -3059,6 +3067,10 @@ deprecated = [ {file = "Deprecated-1.2.12-py2.py3-none-any.whl", hash = "sha256:08452d69b6b5bc66e8330adde0a4f8642e969b9e1702904d137eeb29c8ffc771"}, {file = "Deprecated-1.2.12.tar.gz", hash = "sha256:6d2de2de7931a968874481ef30208fd4e08da39177d61d3d4ebdf4366e7dbca1"}, ] +detecta = [ + {file = "detecta-0.0.5-py3-none-any.whl", hash = "sha256:dbaf1938e5d785386c904034e2553824e2aa31793e967984ca65983aca06d23e"}, + {file = "detecta-0.0.5.tar.gz", hash = "sha256:d2ea7d13dfbbc994d6ce385a7f8dc0a85fe675a8a8e712a64ec56e54c40603ed"}, +] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, @@ -4288,6 +4300,7 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c658432d8a20e95398f6bb95ff9731ce9dfa343fdf21eea7ec6a7edfacd4b4d9"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9dfa564ef27e8e674aa1cc74378416d580ac4ede1136c13dd555a87996e13422"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9c6097b6a9b2bafc5e0f31f659e6ab5e131383209c30c9e978c5b8abdac5ed2a"}, + {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5580eba7345a4d3b097be2f067cc71a306c44bab19e8717a30361f279c929bea"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win32.whl", hash = "sha256:7b04691eb2f41d2c68dbda8d1bd3cb4ef421bdc43aaa56aeb6c762224552dfb6"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win_amd64.whl", hash = "sha256:1adf483e91007a87171d7ce58c34b058eb5dab01b5fee6052f15841778a8ecd8"}, {file = "scikit_learn-0.24.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:ddb52d088889f5596bc4d1de981f2eca106b58243b6679e4782f3ba5096fd645"}, @@ -4295,6 +4308,7 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0567a2d29ad08af98653300c623bd8477b448fe66ced7198bef4ed195925f082"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:99349d77f54e11f962d608d94dfda08f0c9e5720d97132233ebdf35be2858b2d"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:83b21ff053b1ff1c018a2d24db6dd3ea339b1acfbaa4d9c881731f43748d8b3b"}, + {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:abe835a851610f87201819cb315f8d554e1a3e8128912783a31e87264ba5ffb7"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win32.whl", hash = "sha256:c3deb3b19dd9806acf00cf0d400e84562c227723013c33abefbbc3cf906596e9"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d54dbaadeb1425b7d6a66bf44bee2bb2b899fe3e8850b8e94cfb9c904dcb46d0"}, {file = "scikit_learn-0.24.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:3c4f07f47c04e81b134424d53c3f5e16dfd7f494e44fd7584ba9ce9de2c5e6c1"}, @@ -4302,6 +4316,7 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4ddd2b6f7449a5d539ff754fa92d75da22de261fd8fdcfb3596799fadf255101"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:826b92bf45b8ad80444814e5f4ac032156dd481e48d7da33d611f8fe96d5f08b"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:259ec35201e82e2db1ae2496f229e63f46d7f1695ae68eef9350b00dc74ba52f"}, + {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:9599a3f3bf33f73fed0fe06d1dfa4e6081365a58c1c807acb07271be0dce9733"}, {file = "scikit_learn-0.24.1-cp38-cp38-win32.whl", hash = "sha256:8772b99d683be8f67fcc04789032f1b949022a0e6880ee7b75a7ec97dbbb5d0b"}, {file = "scikit_learn-0.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:ed9d65594948678827f4ff0e7ae23344e2f2b4cabbca057ccaed3118fdc392ca"}, {file = "scikit_learn-0.24.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8aa1b3ac46b80eaa552b637eeadbbce3be5931e4b5002b964698e33a1b589e1e"}, @@ -4309,6 +4324,7 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:087dfede39efb06ab30618f9ab55a0397f29c38d63cd0ab88d12b500b7d65fd7"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:895dbf2030aa7337649e36a83a007df3c9811396b4e2fa672a851160f36ce90c"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9a24d1ccec2a34d4cd3f2a1f86409f3f5954cc23d4d2270ba0d03cf018aa4780"}, + {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:54be0a60a5a35005ad69c75902e0f5c9f699db4547ead427e97ef881c3242e6f"}, {file = "scikit_learn-0.24.1-cp39-cp39-win32.whl", hash = "sha256:fab31f48282ebf54dd69f6663cd2d9800096bad1bb67bbc9c9ac84eb77b41972"}, {file = "scikit_learn-0.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:4562dcf4793e61c5d0f89836d07bc37521c3a1889da8f651e2c326463c4bd697"}, ] diff --git a/pyproject.toml b/pyproject.toml index f4c3f068266..fb6675cf9a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ termcolor = "^1.1.0" alpaca-trade-api = "^1.0.1" pyally = "^1.1.2" pycoingecko = "^1.4.0" +detecta = "^0.0.5" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/requirements-full.txt b/requirements-full.txt index 24784254cfe..2ca49ab52d3 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -32,6 +32,7 @@ dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_ decorator==4.4.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6") defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +detecta==0.0.5 entrypoints==0.3; python_version >= "3.6" ephem==3.7.7.1; python_version >= "2.7" and python_version < "4" fbprophet==0.6 diff --git a/requirements-old.txt b/requirements-old.txt index 6aaede2a75a..21a289ed40a 100644 --- a/requirements-old.txt +++ b/requirements-old.txt @@ -23,6 +23,7 @@ cython==0.29.17; python_version >= "3.6" and python_full_version < "3.0.0" or py dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.6.2" decorator==4.4.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6" deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +detecta==0.0.5 ephem==3.7.7.1; python_version >= "2.7" and python_version < "4" fbprophet==0.6 filelock==3.0.12; python_full_version >= "3.6.0" and python_version >= "3.6" diff --git a/requirements-win.txt b/requirements-win.txt index 7cebb528db2..0d6c1686522 100644 --- a/requirements-win.txt +++ b/requirements-win.txt @@ -23,6 +23,7 @@ cython==0.29.17; python_version >= "3.6" and python_full_version < "3.0.0" or py dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.6.2" decorator==4.4.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6" deprecated==1.2.11; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +detecta==0.0.5 ephem==3.7.7.1; python_version >= "2.7" and python_version < "4" fbprophet==0.7.1 filelock==3.0.12; python_full_version >= "3.6.0" and python_version >= "3.6" diff --git a/requirements.txt b/requirements.txt index 919b6242f57..67f988879b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ cycler==0.10.0; python_version >= "3.6" dataclasses==0.8; python_version >= "3.6" and python_version < "3.7" decorator==4.4.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.6") defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +detecta==0.0.5 entrypoints==0.3; python_version >= "3.6" finviz==1.3.4 fredapi==0.4.3 diff --git a/terminal.py b/terminal.py index e9e9665eabc..f4ae5fde0b2 100644 --- a/terminal.py +++ b/terminal.py @@ -6,6 +6,7 @@ import sys import os from datetime import datetime, timedelta import pandas as pd +import yfinance as yf from alpha_vantage.timeseries import TimeSeries from prompt_toolkit.completion import NestedCompleter @@ -23,6 +24,7 @@ from gamestonk_terminal.papermill import papermill_controller as mill from gamestonk_terminal.behavioural_analysis import ba_controller from gamestonk_terminal.technical_analysis import ta_menu as tam from gamestonk_terminal.comparison_analysis import ca_controller +from gamestonk_terminal.exploratory_data_analysis import eda_controller from gamestonk_terminal.options import op_controller from gamestonk_terminal.fred import fred_menu as fm from gamestonk_terminal.portfolio import port_controller @@ -75,6 +77,7 @@ def main(): "fa", "ta", "dd", + "eda", "pred", "ca", "op", @@ -202,6 +205,26 @@ def main(): elif ns_known_args.opt == "dd": b_quit = ddm.dd_menu(df_stock, s_ticker, s_start, s_interval) + elif ns_known_args.opt == "eda": + if s_interval == "1440min": + b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + else: + df_stock = yf.download(s_ticker, start=s_start, progress=False) + df_stock = df_stock.rename( + columns={ + "Open": "1. open", + "High": "2. high", + "Low": "3. low", + "Close": "4. close", + "Adj Close": "5. adjusted close", + "Volume": "6. volume", + } + ) + df_stock.index.name = "date" + s_interval = "1440min" + + b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + elif ns_known_args.opt == "op": b_quit = op_controller.menu(s_ticker)