"""Crypto Technical Analysis Controller Module""" __docformat__ = "numpy" # pylint:disable=too-many-lines import argparse import difflib from typing import List, Union from datetime import datetime from colorama import Style import pandas as pd from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import ( EXPORT_BOTH_RAW_DATA_AND_FIGURES, get_flair, parse_known_args_and_warn, check_positive_list, check_positive, valid_date, try_except, system_clear, ) from gamestonk_terminal.menu import session from gamestonk_terminal.common.technical_analysis import ( custom_indicators_view, momentum_view, overlap_view, trend_indicators_view, volatility_view, volume_view, ) class TechnicalAnalysisController: """Technical Analysis Controller class""" # Command choices CHOICES = [ "cls", "home", "h", "?", "help", "q", "quit", "..", "exit", "r", "reset", ] CHOICES_COMMANDS = [ "ema", "sma", "vwap", "zlma", "cci", "macd", "rsi", "stoch", "fisher", "cg", "adx", "aroon", "bbands", "donchian", "ad", "obv", "fib", ] CHOICES += CHOICES_COMMANDS def __init__( self, ticker: str, start: datetime, interval: str, stock: pd.DataFrame, queue: List[str] = None, ): """Constructor""" self.ta_parser = argparse.ArgumentParser(add_help=False, prog="ta") self.ta_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} self.completer = NestedCompleter.from_nested_dict(choices) if queue: self.queue = queue else: self.queue = list() self.ticker = ticker self.start = start self.interval = interval self.stock = stock self.stock["Adj Close"] = stock["Close"] def print_help(self): """Print help""" s_intraday = (f"Interval: Intraday {self.interval}", "Daily")[ self.interval == "1440min" ] if self.start: crypto_str = f"{s_intraday}\nCrypto: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})" else: crypto_str = f"{s_intraday}\nCrypto: {self.ticker}" dim = Style.DIM if "Volume" not in self.stock else "" not_dim = Style.RESET_ALL if "Volume" not in self.stock else "" help_str = f""" Technical Analysis Menu: {crypto_str} Overlap: ema exponential moving average sma simple moving average zlma zero lag moving average{dim} vwap volume weighted average price{not_dim} Momentum: cci commodity channel index macd moving average convergence/divergence rsi relative strength index stoch stochastic oscillator fisher fisher transform cg centre of gravity Trend: adx average directional movement index aroon aroon indicator Volatility: bbands bollinger bands donchian donchian channels{dim} Volume: ad accumulation/distribution line values obv on balance volume{not_dim} Custom: fib fibonacci retracement """ print(help_str) def switch(self, an_input: str): """Process and dispatch input Parameters ------- an_input : str string with input arguments 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.ta_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, "ta") if self.ticker: self.queue.insert(0, f"load {self.ticker}") self.queue.insert(0, "crypto") self.queue.insert(0, "reset") self.queue.insert(0, "quit") self.queue.insert(0, "quit") # TODO: Go through all models and make sure all needed columns are in dfs @try_except def call_ema(self, other_args: List[str]): """Process ema command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="ema", description=""" The Exponential Moving Average is a staple of technical analysis and is used in countless technical indicators. In a Simple Moving Average, each value in the time period carries equal weight, and values outside of the time period are not included in the average. However, the Exponential Moving Average is a cumulative calculation, including all data. Past values have a diminishing contribution to the average, while more recent values have a greater contribution. This method allows the moving average to be more responsive to changes in the data. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive_list, default=[20, 50], help="Window lengths. Multiple values indicated as comma separated values.", ) parser.add_argument( "-o", "--offset", action="store", dest="n_offset", type=int, default=0, help="offset", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: overlap_view.view_ma( ma_type="EMA", s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, offset=ns_parser.n_offset, export=ns_parser.export, ) @try_except def call_sma(self, other_args: List[str]): """Process sma command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="sma", description=""" Moving Averages are used to smooth the data in an array to help eliminate noise and identify trends. The Simple Moving Average is literally the simplest form of a moving average. Each output value is the average of the previous n values. In a Simple Moving Average, each value in the time period carries equal weight, and values outside of the time period are not included in the average. This makes it less responsive to recent changes in the data, which can be useful for filtering out those changes. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive_list, default=[20, 50], help="Window lengths. Multiple values indicated as comma separated values. ", ) parser.add_argument( "-o", "--offset", action="store", dest="n_offset", type=int, default=0, help="offset", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: overlap_view.view_ma( ma_type="SMA", s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, offset=ns_parser.n_offset, export=ns_parser.export, ) @try_except def call_zlma(self, other_args: List[str]): """Process zlma command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="zlma", description=""" The zero lag exponential moving average (ZLEMA) indicator was created by John Ehlers and Ric Way. The idea is do a regular exponential moving average (EMA) calculation but on a de-lagged data instead of doing it on the regular data. Data is de-lagged by removing the data from "lag" days ago thus removing (or attempting to) the cumulative effect of the moving average. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive_list, default=[20], help="Window lengths. Multiple values indicated as comma separated values.", ) parser.add_argument( "-o", "--offset", action="store", dest="n_offset", type=int, default=0, help="offset", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: overlap_view.view_ma( ma_type="ZLMA", s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, offset=ns_parser.n_offset, export=ns_parser.export, ) @try_except def call_vwap(self, other_args: List[str]): """Process vwap command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="vwap", description=""" The Volume Weighted Average Price that measures the average typical price by volume. It is typically used with intraday charts to identify general direction. """, ) parser.add_argument( "-o", "--offset", action="store", dest="n_offset", type=int, default=0, help="offset", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: if self.interval == "1440min": print("VWAP should be used with intraday data.\n") overlap_view.view_vwap( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, offset=ns_parser.n_offset, export=ns_parser.export, ) @try_except def call_cci(self, other_args: List[str]): """Process cci command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="cci", description=""" The CCI is designed to detect beginning and ending market trends. The range of 100 to -100 is the normal trading range. CCI values outside of this range indicate overbought or oversold conditions. You can also look for price divergence in the CCI. If the price is making new highs, and the CCI is not, then a price correction is likely. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive, default=14, help="length", ) parser.add_argument( "-s", "--scalar", action="store", dest="n_scalar", type=check_positive, default=0.015, help="scalar", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.plot_cci( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, scalar=ns_parser.n_scalar, export=ns_parser.export, ) @try_except def call_macd(self, other_args: List[str]): """Process macd command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="macd", description=""" The Moving Average Convergence Divergence (MACD) is the difference between two Exponential Moving Averages. The Signal line is an Exponential Moving Average of the MACD. \n \n The MACD signals trend changes and indicates the start of new trend direction. High values indicate overbought conditions, low values indicate oversold conditions. Divergence with the price indicates an end to the current trend, especially if the MACD is at extreme high or low values. When the MACD line crosses above the signal line a buy signal is generated. When the MACD crosses below the signal line a sell signal is generated. To confirm the signal, the MACD should be above zero for a buy, and below zero for a sell. """, ) parser.add_argument( "-f", "--fast", action="store", dest="n_fast", type=check_positive, default=12, help="The short period.", ) parser.add_argument( "-s", "--slow", action="store", dest="n_slow", type=check_positive, default=26, help="The long period.", ) parser.add_argument( "--signal", action="store", dest="n_signal", type=check_positive, default=9, help="The signal period.", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.view_macd( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, n_fast=ns_parser.n_fast, n_slow=ns_parser.n_slow, n_signal=ns_parser.n_signal, export=ns_parser.export, ) @try_except def call_rsi(self, other_args: List[str]): """Process rsi command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="rsi", description=""" The Relative Strength Index (RSI) calculates a ratio of the recent upward price movements to the absolute price movement. The RSI ranges from 0 to 100. The RSI is interpreted as an overbought/oversold indicator when the value is over 70/below 30. You can also look for divergence with price. If the price is making new highs/lows, and the RSI is not, it indicates a reversal. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive, default=14, help="length", ) parser.add_argument( "-s", "--scalar", action="store", dest="n_scalar", type=check_positive, default=100, help="scalar", ) parser.add_argument( "-d", "--drift", action="store", dest="n_drift", type=check_positive, default=1, help="drift", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.view_rsi( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, scalar=ns_parser.n_scalar, drift=ns_parser.n_drift, export=ns_parser.export, ) @try_except def call_stoch(self, other_args: List[str]): """Process stoch command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="stoch", description=""" The Stochastic Oscillator measures where the close is in relation to the recent trading range. The values range from zero to 100. %D values over 75 indicate an overbought condition; values under 25 indicate an oversold condition. When the Fast %D crosses above the Slow %D, it is a buy signal; when it crosses below, it is a sell signal. The Raw %K is generally considered too erratic to use for crossover signals. """, ) parser.add_argument( "-k", "--fastkperiod", action="store", dest="n_fastkperiod", type=check_positive, default=14, help="The time period of the fastk moving average", ) parser.add_argument( "-d", "--slowdperiod", action="store", dest="n_slowdperiod", type=check_positive, default=3, help="The time period of the slowd moving average", ) parser.add_argument( "--slowkperiod", action="store", dest="n_slowkperiod", type=check_positive, default=3, help="The time period of the slowk moving average", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.view_stoch( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, fastkperiod=ns_parser.n_fastkperiod, slowdperiod=ns_parser.n_slowdperiod, slowkperiod=ns_parser.n_slowkperiod, export=ns_parser.export, ) @try_except def call_fisher(self, other_args: List[str]): """Process fisher command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="fisher", description=""" The Fisher Transform is a technical indicator created by John F. Ehlers that converts prices into a Gaussian normal distribution.1 The indicator highlights when prices have moved to an extreme, based on recent prices. This may help in spotting turning points in the price of an asset. It also helps show the trend and isolate the price waves within a trend. """, ) 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_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.view_fisher( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, export=ns_parser.export, ) @try_except def call_cg(self, other_args: List[str]): """Process cg command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="cg", description=""" The Center of Gravity indicator, in short, is used to anticipate future price movements and to trade on price reversals as soon as they happen. However, just like other oscillators, the COG indicator returns the best results in range-bound markets and should be avoided when the price is trending. Traders who use it will be able to closely speculate the upcoming price change of the asset. """, ) 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_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: momentum_view.view_cg( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, export=ns_parser.export, ) @try_except def call_adx(self, other_args: List[str]): """Process adx command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="adx", description=""" The ADX is a Welles Wilder style moving average of the Directional Movement Index (DX). The values range from 0 to 100, but rarely get above 60. To interpret the ADX, consider a high number to be a strong trend, and a low number, a weak trend. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive, default=14, help="length", ) parser.add_argument( "-s", "--scalar", action="store", dest="n_scalar", type=check_positive, default=100, help="scalar", ) parser.add_argument( "-d", "--drift", action="store", dest="n_drift", type=check_positive, default=1, help="drift", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: trend_indicators_view.plot_adx( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, scalar=ns_parser.n_scalar, drift=ns_parser.n_drift, export=ns_parser.export, ) @try_except def call_aroon(self, other_args: List[str]): """Process aroon command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="aroon", description=""" The word aroon is Sanskrit for "dawn's early light." The Aroon indicator attempts to show when a new trend is dawning. The indicator consists of two lines (Up and Down) that measure how long it has been since the highest high/lowest low has occurred within an n period range. \n \n When the Aroon Up is staying between 70 and 100 then it indicates an upward trend. When the Aroon Down is staying between 70 and 100 then it indicates an downward trend. A strong upward trend is indicated when the Aroon Up is above 70 while the Aroon Down is below 30. Likewise, a strong downward trend is indicated when the Aroon Down is above 70 while the Aroon Up is below 30. Also look for crossovers. When the Aroon Down crosses above the Aroon Up, it indicates a weakening of the upward trend (and vice versa). """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive, default=25, help="length", ) parser.add_argument( "-s", "--scalar", action="store", dest="n_scalar", type=check_positive, default=100, help="scalar", ) parser.add_argument( "-o", "--offset", action="store", dest="n_offset", type=check_positive, default=0, help="offset", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: trend_indicators_view.plot_aroon( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, scalar=ns_parser.n_scalar, export=ns_parser.export, ) @try_except def call_bbands(self, other_args: List[str]): """Process bbands command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="bbands", description=""" Bollinger Bands consist of three lines. The middle band is a simple moving average (generally 20 periods) of the typical price (TP). The upper and lower bands are F standard deviations (generally 2) above and below the middle band. The bands widen and narrow when the volatility of the price is higher or lower, respectively. \n \nBollinger Bands do not, in themselves, generate buy or sell signals; they are an indicator of overbought or oversold conditions. When the price is near the upper or lower band it indicates that a reversal may be imminent. The middle band becomes a support or resistance level. The upper and lower bands can also be interpreted as price targets. When the price bounces off of the lower band and crosses the middle band, then the upper band becomes the price target. """, ) parser.add_argument( "-l", "--length", action="store", dest="n_length", type=check_positive, default=5, help="length", ) parser.add_argument( "-s", "--std", action="store", dest="n_std", type=check_positive, default=2, help="std", ) parser.add_argument( "-m", "--mamode", action="store", dest="s_mamode", default="sma", help="mamode", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: volatility_view.view_bbands( ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, length=ns_parser.n_length, n_std=ns_parser.n_std, mamode=ns_parser.s_mamode, export=ns_parser.export, ) @try_except def call_donchian(self, other_args: List[str]): """Process donchian command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="donchian", description=""" Donchian Channels are three lines generated by moving average calculations that comprise an indicator formed by upper and lower bands around a midrange or median band. The upper band marks the highest price of a security over N periods while the lower band marks the lowest price of a security over N periods. The area between the upper and lower bands represents the Donchian Channel. """, ) parser.add_argument( "-u", "--length_upper", action="store", dest="n_length_upper", type=check_positive, default=20, help="length", ) parser.add_argument( "-l", "--length_lower", action="store", dest="n_length_lower", type=check_positive, default=20, help="length", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: volatility_view.view_donchian( ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, upper_length=ns_parser.n_length_upper, lower_length=ns_parser.n_length_lower, export=ns_parser.export, ) @try_except def call_ad(self, other_args: List[str]): """Process ad command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="ad", description=""" The Accumulation/Distribution Line is similar to the On Balance Volume (OBV), which sums the volume times +1/-1 based on whether the close is higher than the previous close. The Accumulation/Distribution indicator, however multiplies the volume by the close location value (CLV). The CLV is based on the movement of the issue within a single bar and can be +1, -1 or zero. \n \n The Accumulation/Distribution Line is interpreted by looking for a divergence in the direction of the indicator relative to price. If the Accumulation/Distribution Line is trending upward it indicates that the price may follow. Also, if the Accumulation/Distribution Line becomes flat while the price is still rising (or falling) then it signals an impending flattening of the price. """, ) parser.add_argument( "--open", action="store_true", default=False, dest="b_use_open", help="uses open value of stock", ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: volume_view.plot_ad( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, use_open=ns_parser.b_use_open, export=ns_parser.export, ) @try_except def call_obv(self, other_args: List[str]): """Process obv command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="obv", description=""" The On Balance Volume (OBV) is a cumulative total of the up and down volume. When the close is higher than the previous close, the volume is added to the running total, and when the close is lower than the previous close, the volume is subtracted from the running total. \n \n To interpret the OBV, look for the OBV to move with the price or precede price moves. If the price moves before the OBV, then it is a non-confirmed move. A series of rising peaks, or falling troughs, in the OBV indicates a strong trend. If the OBV is flat, then the market is not trending. """, ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: volume_view.plot_obv( s_ticker=self.ticker, s_interval=self.interval, df_stock=self.stock, export=ns_parser.export, ) @try_except def call_fib(self, other_args: List[str]): """Process fib command""" parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="fib", description="Calculates the fibonacci retracement levels", ) parser.add_argument( "-p", "--period", dest="period", type=int, help="Days to lookback for retracement", default=120, ) parser.add_argument( "--start", dest="start", type=valid_date, help="Starting date to select", required="--end" in other_args, ) parser.add_argument( "--end", dest="end", type=valid_date, help="Ending date to select", required="--start" in other_args, ) ns_parser = parse_known_args_and_warn( parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: custom_indicators_view.fibonacci_retracement( s_ticker=self.ticker, df_stock=self.stock, period=ns_parser.period, start_date=ns_parser.start, end_date=ns_parser.end, export=ns_parser.export, ) def menu( ticker: str, start: datetime, interval: str, stock: pd.DataFrame, queue: List[str] = None, ): """Technical Analysis Menu""" ta_controller = TechnicalAnalysisController(ticker, start, interval, stock, queue) an_input = "HELP_ME" while True: # There is a command in the queue if ta_controller.queue and len(ta_controller.queue) > 0: # If the command is quitting the menu we want to return in here if ta_controller.queue[0] in ("q", "..", "quit"): if len(ta_controller.queue) > 1: return ta_controller.queue[1:] return [] # Consume 1 element from the queue an_input = ta_controller.queue[0] ta_controller.queue = ta_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 ta_controller.CHOICES_COMMANDS: print(f"{get_flair()} /crypto/ta/ $ {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": ta_controller.print_help() # Get input from user using auto-completion if session and gtff.USE_PROMPT_TOOLKIT and ta_controller.completer: try: an_input = session.prompt( f"{get_flair()} /crypto/ta/ $ ", completer=ta_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()} /crypto/ta/ $ ") try: # Process the input command ta_controller.queue = ta_controller.switch(an_input) except SystemExit: print( f"\nThe command '{an_input}' doesn't exist on the /stocks/options menu.", end="", ) similar_cmd = difflib.get_close_matches( an_input.split(" ")[0] if " " in an_input else an_input, ta_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 = "" ta_controller.queue = [] print("\n") continue an_input = candidate_input else: an_input = similar_cmd[0] print(f" Replacing by '{an_input}'.") ta_controller.queue.insert(0, an_input) else: print("\n")