Files
OpenBB/gamestonk_terminal/stocks/quantitative_analysis/qa_controller.py
2022-01-02 22:03:36 +00:00

959 lines
31 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.

"""Quantitative Analysis Controller Module"""
__docformat__ = "numpy"
import argparse
import difflib
from typing import List, Union
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal.common.quantitative_analysis import (
qa_view,
rolling_view,
)
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.stocks import stocks_helper
from gamestonk_terminal.helper_funcs import (
EXPORT_ONLY_RAW_DATA_ALLOWED,
get_flair,
check_positive,
check_proportion_range,
parse_known_args_and_warn,
try_except,
system_clear,
valid_date,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.stocks.quantitative_analysis.factors_view import capm_view
class QaController:
"""Quantitative Analysis Controller class"""
# Command choices
CHOICES = [
"cls",
"home",
"h",
"?",
"help",
"q",
"quit",
"..",
"exit",
"r",
"reset",
]
CHOICES_COMMANDS = [
"load",
"pick",
"raw",
"summary",
"hist",
"cdf",
"bw",
"rolling",
"decompose",
"cusum",
"acf",
"spread",
"quantile",
"skew",
"kurtosis",
"normality",
"qqplot",
"unitroot",
"goodness",
"unitroot",
"capm",
]
CHOICES += CHOICES_COMMANDS
stock_interval = [1, 5, 15, 30, 60]
stock_sources = ["yf", "av", "iex"]
def __init__(
self,
ticker: str,
start: datetime,
interval: str,
stock: pd.DataFrame,
queue: List[str] = None,
):
"""Constructor"""
stock["Returns"] = stock["Adj Close"].pct_change()
stock["LogRet"] = np.log(stock["Adj Close"]) - np.log(
stock["Adj Close"].shift(1)
)
stock = stock.rename(columns={"Adj Close": "AdjClose"})
stock = stock.dropna()
self.stock = stock
self.ticker = ticker
self.start = start
self.interval = interval
self.target = "Returns"
self.df_columns = list(stock.columns)
self.qa_parser = argparse.ArgumentParser(add_help=False, prog="qa")
self.qa_parser.add_argument(
"cmd",
choices=self.CHOICES,
)
self.completer: Union[None, NestedCompleter] = None
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.CHOICES}
choices["pick"]["-t"] = {c: None for c in self.df_columns}
choices["pick"]["--target"] = {c: None for c in self.df_columns}
choices["load"]["-i"] = {c: None for c in self.stock_interval}
choices["load"]["--interval"] = {c: None for c in self.stock_interval}
choices["load"]["--source"] = {c: None for c in self.stock_sources}
self.completer = NestedCompleter.from_nested_dict(choices)
if queue:
self.queue = queue
else:
self.queue = list()
def print_help(self):
"""Print help"""
s_intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"]
if self.start:
stock_str = f"{s_intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})"
else:
stock_str = f"{s_intraday} Stock: {self.ticker}"
help_str = f"""
Quantitative Analysis:
{stock_str}
Target Column: {self.target}
Statistics:
summary brief summary statistics of loaded stock.
normality normality statistics and tests
unitroot unit root test for stationarity (ADF, KPSS)
Plots:
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
Rolling Metrics:
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
Other:
raw print raw data
decompose decomposition in cyclic-trend, season, and residuals of prices
cusum detects abrupt changes using cumulative sum algorithm of prices
capm capital asset pricing model
"""
print(help_str)
def switch(self, an_input: str):
"""Process and dispatch input
Returns
-------
List[str]
List of commands in the queue to execute
"""
# Empty command
if not an_input:
print("")
return self.queue
# Navigation slash is being used
if "/" in an_input:
actions = an_input.split("/")
# Absolute path is specified
if not actions[0]:
an_input = "home"
# Relative path so execute first instruction
else:
an_input = actions[0]
# Add all instructions to the queue
for cmd in actions[1:][::-1]:
if cmd:
self.queue.insert(0, cmd)
(known_args, other_args) = self.qa_parser.parse_known_args(an_input.split())
# Redirect commands to their correct functions
if known_args.cmd:
if known_args.cmd in ("..", "q"):
known_args.cmd = "quit"
elif known_args.cmd in ("?", "h"):
known_args.cmd = "help"
elif known_args.cmd == "r":
known_args.cmd = "reset"
getattr(
self,
"call_" + known_args.cmd,
lambda _: "Command not recognized!",
)(other_args)
return self.queue
def call_cls(self, _):
"""Process cls command"""
system_clear()
def call_home(self, _):
"""Process home command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_help(self, _):
"""Process help command"""
self.print_help()
def call_quit(self, _):
"""Process quit menu command"""
print("")
self.queue.insert(0, "quit")
def call_exit(self, _):
"""Process exit terminal command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_reset(self, _):
"""Process reset command"""
self.queue.insert(0, "qa")
if self.ticker:
self.queue.insert(0, f"load {self.ticker}")
self.queue.insert(0, "stocks")
self.queue.insert(0, "reset")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_load(self, other_args: List[str]):
"""Process load command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="load",
description="Load stock ticker to perform analysis on. When the data source is 'yf', an Indian ticker can be"
" loaded by using '.NS' at the end, e.g. 'SBIN.NS'. See available market in"
" https://help.yahoo.com/kb/exchanges-data-providers-yahoo-finance-sln2310.html.",
)
parser.add_argument(
"-t",
"--ticker",
action="store",
dest="ticker",
required="-h" not in other_args,
help="Stock ticker",
)
parser.add_argument(
"-s",
"--start",
type=valid_date,
default=(datetime.now() - timedelta(days=366)).strftime("%Y-%m-%d"),
dest="start",
help="The starting date (format YYYY-MM-DD) of the stock",
)
parser.add_argument(
"-e",
"--end",
type=valid_date,
default=datetime.now().strftime("%Y-%m-%d"),
dest="end",
help="The ending date (format YYYY-MM-DD) of the stock",
)
parser.add_argument(
"-i",
"--interval",
action="store",
dest="interval",
type=int,
default=1440,
choices=self.stock_interval,
help="Intraday stock minutes",
)
parser.add_argument(
"--source",
action="store",
dest="source",
choices=self.stock_sources,
default="yf",
help="Source of historical data.",
)
parser.add_argument(
"-p",
"--prepost",
action="store_true",
default=False,
dest="prepost",
help="Pre/After market hours. Only works for 'yf' source, and intraday data",
)
# For the case where a user uses: 'load BB'
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:
df_stock_candidate = stocks_helper.load(
ns_parser.ticker,
ns_parser.start,
ns_parser.interval,
ns_parser.end,
ns_parser.prepost,
ns_parser.source,
)
if not df_stock_candidate.empty:
self.stock = df_stock_candidate
if "." in ns_parser.ticker:
self.ticker = ns_parser.ticker.upper().split(".")[0]
else:
self.ticker = ns_parser.ticker.upper()
self.start = ns_parser.start
self.interval = str(ns_parser.interval) + "min"
self.stock["Returns"] = self.stock["Adj Close"].pct_change()
self.stock["LogRet"] = np.log(self.stock["Adj Close"]) - np.log(
self.stock["Adj Close"].shift(1)
)
self.stock = self.stock.rename(columns={"Adj Close": "AdjClose"})
self.stock = self.stock.dropna()
@try_except
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",
choices=list(self.stock.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
print("")
@try_except
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(
"-n",
"--num",
help="Number to show",
type=check_positive,
default=20,
dest="num",
)
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.stock[self.target],
num=ns_parser.num,
sort="",
des=ns_parser.descend,
export=ns_parser.export,
)
@try_except
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.stock, export=ns_parser.export)
@try_except
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.stock,
target=self.target,
bins=ns_parser.n_bins,
)
@try_except
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.stock,
target=self.target,
export=ns_parser.export,
)
@try_except
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.stock,
target=self.target,
yearly=ns_parser.year,
)
@try_except
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.stock,
target=self.target,
multiplicative=ns_parser.multiplicative,
export=ns_parser.export,
)
@try_except
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.stock[self.target].values)
- min(self.stock[self.target].values)
)
/ 40,
help="threshold",
)
parser.add_argument(
"-d",
"--drift",
dest="drift",
type=float,
default=(
max(self.stock[self.target].values)
- min(self.stock[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.stock,
target=self.target,
threshold=ns_parser.threshold,
drift=ns_parser.drift,
)
@try_except
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(
"-l",
"--lags",
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:
if self.target != "AdjClose":
print(
"Target not AdjClose. For best results, use `pick AdjClose` first."
)
qa_view.display_acf(
name=self.ticker,
df=self.stock,
target=self.target,
lags=ns_parser.lags,
)
@try_except
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(
"-l",
"--length",
action="store",
dest="n_length",
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.stock,
target=self.target,
length=ns_parser.n_length,
export=ns_parser.export,
)
@try_except
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(
"-l",
"--length",
action="store",
dest="n_length",
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.stock,
target=self.target,
length=ns_parser.n_length,
export=ns_parser.export,
)
@try_except
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<q<1).
""",
)
parser.add_argument(
"-l",
"--length",
action="store",
dest="n_length",
type=check_positive,
default=14,
help="length",
)
parser.add_argument(
"-q",
"--quantile",
action="store",
dest="f_quantile",
type=check_proportion_range,
default=0.5,
help="quantile",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
rolling_view.display_quantile(
name=self.ticker,
df=self.stock,
target=self.target,
length=ns_parser.n_length,
quantile=ns_parser.f_quantile,
export=ns_parser.export,
)
@try_except
def call_skew(self, other_args: List[str]):
"""Process skew command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="skew",
description="""
Skewness is a measure of asymmetry or distortion of symmetric
distribution. It measures the deviation of the given distribution
of a random variable from a symmetric distribution, such as normal
distribution. A normal distribution is without any skewness, as it is
symmetrical on both sides. Hence, a curve is regarded as skewed if
it is shifted towards the right or the left.
""",
)
parser.add_argument(
"-l",
"--length",
action="store",
dest="n_length",
type=check_positive,
default=14,
help="length",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
rolling_view.display_skew(
name=self.ticker,
df=self.stock,
target=self.target,
length=ns_parser.n_length,
export=ns_parser.export,
)
@try_except
def call_kurtosis(self, other_args: List[str]):
"""Process kurtosis command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="kurtosis",
description="""
Kurtosis is a measure of the "tailedness" of the probability distribution
of a real-valued random variable. Like skewness, kurtosis describes the shape
of a probability distribution and there are different ways of quantifying it
for a theoretical distribution and corresponding ways of estimating it from
a sample from a population. Different measures of kurtosis may have different
interpretations.
""",
)
parser.add_argument(
"-l",
"--length",
action="store",
dest="n_length",
type=check_positive,
default=14,
help="length",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
rolling_view.display_kurtosis(
name=self.ticker,
df=self.stock,
target=self.target,
length=ns_parser.n_length,
export=ns_parser.export,
)
@try_except
def call_normality(self, other_args: List[str]):
"""Process normality command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="normality",
description="""
Normality tests
""",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
qa_view.display_normality(
df=self.stock, target=self.target, export=ns_parser.export
)
@try_except
def call_qqplot(self, other_args: List[str]):
"""Process qqplot command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="qqplot",
description="""
Display QQ plot vs normal quantiles
""",
)
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
qa_view.display_qqplot(name=self.ticker, df=self.stock, target=self.target)
@try_except
def call_unitroot(self, other_args: List[str]):
"""Process unitroot command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="unitroot",
description="""
Unit root test / stationarity (ADF, KPSS)
""",
)
parser.add_argument(
"-r",
"--fuller_reg",
help="Type of regression. Can be ‘c’,’ct’,’ctt’,’nc’ 'c' - Constant and t - trend order",
choices=["c", "ct", "ctt", "nc"],
default="c",
type=str,
dest="fuller_reg",
)
parser.add_argument(
"-k",
"--kps_reg",
help="Type of regression. Can be ‘c’,’ct'",
choices=["c", "ct"],
type=str,
dest="kpss_reg",
default="c",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
qa_view.display_unitroot(
df=self.stock,
target=self.target,
fuller_reg=ns_parser.fuller_reg,
kpss_reg=ns_parser.kpss_reg,
export=ns_parser.export,
)
@try_except
def call_capm(self, other_args: List[str]):
"""Process capm command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="capm",
description="""
Provides detailed information about a stock's risk compared to the market risk.
""",
)
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
capm_view(self.ticker)
def menu(
ticker: str,
start: datetime,
interval: str,
stock: pd.DataFrame,
queue: List[str] = None,
):
"""Quantitative Analysis Menu"""
qa_controller = QaController(ticker, start, interval, stock, queue)
an_input = "HELP_ME"
while True:
# There is a command in the queue
if qa_controller.queue and len(qa_controller.queue) > 0:
# If the command is quitting the menu we want to return in here
if qa_controller.queue[0] in ("q", "..", "quit"):
print("")
if len(qa_controller.queue) > 1:
return qa_controller.queue[1:]
return []
# Consume 1 element from the queue
an_input = qa_controller.queue[0]
qa_controller.queue = qa_controller.queue[1:]
# Print the current location because this was an instruction and we want user to know what was the action
if an_input and an_input.split(" ")[0] in qa_controller.CHOICES_COMMANDS:
print(f"{get_flair()} /stocks/qa/ $ {an_input}")
# Get input command from user
else:
# Display help menu when entering on this menu from a level above
if an_input == "HELP_ME":
qa_controller.print_help()
# Get input from user using auto-completion
if session and gtff.USE_PROMPT_TOOLKIT and qa_controller.completer:
try:
an_input = session.prompt(
f"{get_flair()} /stocks/qa/ $ ",
completer=qa_controller.completer,
search_ignore_case=True,
)
except KeyboardInterrupt:
# Exit in case of keyboard interrupt
an_input = "exit"
# Get input from user without auto-completion
else:
an_input = input(f"{get_flair()} /stocks/qa/ $ ")
try:
# Process the input command
qa_controller.queue = qa_controller.switch(an_input)
except SystemExit:
print(
f"\nThe command '{an_input}' doesn't exist on the /stocks/qa menu.",
end="",
)
similar_cmd = difflib.get_close_matches(
an_input.split(" ")[0] if " " in an_input else an_input,
qa_controller.CHOICES,
n=1,
cutoff=0.7,
)
if similar_cmd:
if " " in an_input:
candidate_input = (
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
)
if candidate_input == an_input:
an_input = ""
qa_controller.queue = []
print("\n")
continue
an_input = candidate_input
else:
an_input = similar_cmd[0]
print(f" Replacing by '{an_input}'.")
qa_controller.queue.insert(0, an_input)
else:
print("\n")