"""Custom Quantitative Analysis Controller Module""" __docformat__ = "numpy" import argparse import logging from pathlib import Path from typing import List import pandas as pd from prompt_toolkit.completion import NestedCompleter from openbb_terminal import feature_flags as obbff from openbb_terminal.common.quantitative_analysis import qa_view, rolling_view from openbb_terminal.custom import custom_model from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import ( EXPORT_ONLY_FIGURES_ALLOWED, EXPORT_ONLY_RAW_DATA_ALLOWED, check_positive, check_proportion_range, parse_known_args_and_warn, check_list_dates, ) from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) class QaController(BaseController): """Quantitative Analysis Controller class""" CHOICES_COMMANDS = [ "load", "pick", "raw", "summary", "line", "hist", "cdf", "bw", "rolling", "decompose", "cusum", "acf", "spread", "quantile", "skew", "kurtosis", "normality", "qqplot", "unitroot", "goodness", "unitroot", "capm", ] PATH = "/custom/qa/" def __init__( self, custom_df: pd.DataFrame, file: str, queue: List[str] = None, ): """Constructor""" super().__init__(queue) self.DATA_FILES = [file.name for file in Path("custom_imports").iterdir()] self.df = custom_df self.ticker = file self.target = custom_df.columns[1] self.file = file if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} choices["pick"] = {c: None for c in list(custom_df.columns)} choices["load"] = {c: None for c in self.DATA_FILES} self.choices = choices choices["support"] = self.SUPPORT_CHOICES self.completer = NestedCompleter.from_nested_dict(choices) def custom_reset(self): """Class specific component of reset command""" if self.ticker: if self.target: return ["custom", f"load {self.file}", "qa", f"pick {self.target}"] return ["custom", f"load {self.file}", "qa"] return [] def update_runtime_choices(self): if session and obbff.USE_PROMPT_TOOLKIT: self.choices["pick"] = {c: None for c in list(self.df.columns)} self.completer = NestedCompleter.from_nested_dict(self.choices) def print_help(self): """Print help""" help_text = f"""[cmds] load load new data file pick pick target column for analysis[/cmds] [param]File: [/param]{self.file} [param]Target Column: [/param]{self.target} [cmds] [info]Statistics:[/info] summary brief summary statistics of loaded stock. normality normality statistics and tests unitroot unit root test for stationarity (ADF, KPSS) [info]Plots:[/info] line line plot of selected target hist histogram with density plot cdf cumulative distribution function bw box and whisker plot acf (partial) auto-correlation function differentials of prices qqplot residuals against standard normal curve [info]Rolling Metrics:[/info] rolling rolling mean and std deviation of prices spread rolling variance and std deviation of prices quantile rolling median and quantile of prices skew rolling skewness of distribution of prices kurtosis rolling kurtosis of distribution of prices [info]Other:[/info] raw print raw data decompose decomposition in cyclic-trend, season, and residuals of prices cusum detects abrupt changes using cumulative sum algorithm of prices[/cmds] """ console.print(text=help_text, menu="Custom - Quantitative Analysis") @log_start_end(log=logger) def call_load(self, other_args: List[str]): """Process load""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="load", description="Load custom data set into a dataframe", ) parser.add_argument( "-f", "--file", choices=self.DATA_FILES, help="File to load in.", default="test.csv", type=str, ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-f") ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: file = Path("custom_imports") / ns_parser.file self.df = custom_model.load(file) self.df.columns = self.df.columns.map(lambda x: x.lower()) for col in self.df.columns: if col in ["date", "time", "timestamp"]: # Could be others? self.df[col] = pd.to_datetime(self.df[col]) self.df = self.df.set_index(col) self.file = ns_parser.file self.update_runtime_choices() console.print("") @log_start_end(log=logger) def call_pick(self, other_args: List[str]): """Process pick command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="pick", description=""" Change target variable """, ) parser.add_argument( "-t", "--target", dest="target", type=lambda x: x.lower(), choices=list(self.df.columns), help="Select variable to analyze", ) if other_args and "-t" not in other_args and "-h" not in other_args: other_args.insert(0, "-t") ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: self.target = ns_parser.target console.print("") @log_start_end(log=logger) def call_raw(self, other_args: List[str]): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="raw", description=""" Print raw data to console """, ) parser.add_argument( "-w", "--window", help="Number to show", type=check_positive, default=20, dest="limit", ) parser.add_argument( "-d", "--descend", action="store_true", default=False, dest="descend", help="Sort in descending order", ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_raw( self.df[self.target], num=ns_parser.limit, sort="", des=ns_parser.descend, export=ns_parser.export, ) @log_start_end(log=logger) def call_summary(self, other_args: List[str]): """Process summary command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="summary", description=""" Summary statistics """, ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_summary(df=self.df, export=ns_parser.export) @log_start_end(log=logger) def call_line(self, other_args: List[str]): """Process line command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="line", description="Show line plot of selected data and allow to draw lines or highlight specific datetimes.", ) parser.add_argument( "--log", help="Plot with y on log scale", dest="log", action="store_true", default=False, ) parser.add_argument( "-d", "--draw", help="Draw lines and annotate on the plot", dest="draw", action="store_true", default=False, ) parser.add_argument( "--ml", help="Draw vertical line markers to highlight certain events", dest="ml", type=check_list_dates, default="", ) parser.add_argument( "--ms", help="Draw scatter markers to highlight certain events", dest="ms", type=check_list_dates, default="", ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED ) if ns_parser: qa_view.display_line( self.df[self.target], title=f"{self.ticker} {self.target}", log_y=ns_parser.log, draw=ns_parser.draw, markers_lines=ns_parser.ml, markers_scatter=ns_parser.ms, ) @log_start_end(log=logger) def call_hist(self, other_args: List[str]): """Process hist command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="hist", description=""" Histogram with density and rug """, ) parser.add_argument( "-b", "--bins", type=check_positive, default=15, dest="n_bins" ) ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: qa_view.display_hist( name=self.ticker, df=self.df, target=self.target, bins=ns_parser.n_bins, ) @log_start_end(log=logger) def call_cdf(self, other_args: List[str]): """Process cdf command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="cdf", description=""" Cumulative distribution function """, ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_cdf( name=self.ticker, df=self.df, target=self.target, export=ns_parser.export, ) @log_start_end(log=logger) def call_bw(self, other_args: List[str]): """Process bwy command""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False, prog="bw", description=""" Box and Whisker plot """, ) parser.add_argument( "-y", "--yearly", action="store_true", default=False, dest="year", help="Flag to show yearly bw plot", ) ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: qa_view.display_bw( name=self.ticker, df=self.df, target=self.target, yearly=ns_parser.year, ) @log_start_end(log=logger) def call_decompose(self, other_args: List[str]): """Process decompose command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, 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", ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_seasonal( name=self.ticker, df=self.df, target=self.target, multiplicative=ns_parser.multiplicative, export=ns_parser.export, ) @log_start_end(log=logger) def call_cusum(self, other_args: List[str]): """Process cusum command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="cusum", description=""" Cumulative sum algorithm (CUSUM) to detect abrupt changes in data """, ) parser.add_argument( "-t", "--threshold", dest="threshold", type=float, default=( max(self.df[self.target].values) - min(self.df[self.target].values) ) / 40, help="threshold", ) parser.add_argument( "-d", "--drift", dest="drift", type=float, default=( max(self.df[self.target].values) - min(self.df[self.target].values) ) / 80, help="drift", ) ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: qa_view.display_cusum( df=self.df, target=self.target, threshold=ns_parser.threshold, drift=ns_parser.drift, ) @log_start_end(log=logger) def call_acf(self, other_args: List[str]): """Process acf command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="acf", description=""" Auto-Correlation and Partial Auto-Correlation Functions for diff and diff diff stock data """, ) parser.add_argument( "-w", "--window", dest="lags", type=check_positive, default=15, help="maximum lags to display in plots", ) ns_parser = parse_known_args_and_warn(parser, other_args) if ns_parser: qa_view.display_acf( name=self.ticker, df=self.df, target=self.target, lags=ns_parser.lags, ) @log_start_end(log=logger) def call_rolling(self, other_args: List[str]): """Process rolling command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="rolling", description=""" Rolling mean and std deviation """, ) parser.add_argument( "-w", "--window", action="store", dest="n_window", type=check_positive, default=14, help="Window length", ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: rolling_view.display_mean_std( name=self.ticker, df=self.df, target=self.target, window=ns_parser.n_window, export=ns_parser.export, ) @log_start_end(log=logger) def call_spread(self, other_args: List[str]): """Process spread command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="spread", description="""Shows rolling spread measurement """, ) parser.add_argument( "-w", "--window", action="store", dest="n_window", type=check_positive, default=14, help="Window length", ) ns_parser = parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: rolling_view.display_spread( name=self.ticker, df=self.df, target=self.target, window=ns_parser.n_window, export=ns_parser.export, ) @log_start_end(log=logger) def call_quantile(self, other_args: List[str]): """Process quantile command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="quantile", description=""" The quantiles are values which divide the distribution such that there is a given proportion of observations below the quantile. For example, the median is a quantile. The median is the central value of the distribution, such that half the points are less than or equal to it and half are greater than or equal to it. By default, q is set at 0.5, which effectively is median. Change q to get the desired quantile (0