Add TA and test coverage for the Forex context (#1532)

* Add technical analysis to forex menu

* Remove "stock" and "$" from the common ta charts

* Add tests for forex context

* Update forex custom reset functions

* Remove -t arg (ticker) insertion into other_args in forex controller

* Remove excluded command from forex ta help string

* Update test data

Co-authored-by: didierlopes.eth <dro.lopes@campus.fct.unl.pt>
This commit is contained in:
Theodore Aptekarev
2022-03-22 20:24:51 +03:00
committed by GitHub
parent f761db8f98
commit ede6ec8b1a
32 changed files with 28711 additions and 5 deletions

View File

@@ -77,7 +77,7 @@ def display_adx(
ax1.plot(plot_data.index, plot_data["Close"].values)
ax1.set_title(f"Average Directional Movement Index (ADX) on {s_ticker}")
ax1.set_xlim(plot_data.index[0], plot_data.index[-1])
ax1.set_ylabel("Share Price ($)")
ax1.set_ylabel("Price")
theme.style_primary_axis(
ax1,
data_index=plot_data.index.to_list(),
@@ -167,7 +167,7 @@ def display_aroon(
ax1.plot(plot_data.index, plot_data["Adj Close"].values)
ax1.set_title(f"Aroon on {s_ticker}")
ax1.set_xlim(plot_data.index[0], plot_data.index[-1])
ax1.set_ylabel("Share Price ($)")
ax1.set_ylabel("Price")
theme.style_primary_axis(
ax1,
data_index=plot_data.index.to_list(),

View File

@@ -8,6 +8,7 @@ from gamestonk_terminal.helper_classes import ModelsNamespace as _models
# Context menus
from .av_view import display_candle as candle
from .av_view import display_quote as quote
from .technical_analysis import ta_api as ta
from .oanda import oanda_api as oanda
# Models

View File

@@ -27,7 +27,7 @@ class ForexController(BaseController):
"""Forex Controller class."""
CHOICES_COMMANDS = ["to", "from", "load", "quote", "candle", "resources"]
CHOICES_MENUS = ["oanda"]
CHOICES_MENUS = ["ta", "oanda"]
PATH = "/forex/"
FILE_PATH = os.path.join(os.path.dirname(__file__), "README.md")
@@ -60,12 +60,23 @@ class ForexController(BaseController):
quote get last quote
load get historical data
candle show candle plot for loaded data[/cmds]
[menu]
> ta technical analysis for loaded coin, e.g.: ema, macd, rsi, adx, bbands, obv
[/menu]
{has_symbols_end}
[info]Forex brokerages:[/info][menu]
> oanda Oanda menu[/menu][/cmds]
"""
console.print(text=help_text, menu="Forex")
def custom_reset(self):
"""Class specific component of reset command"""
set_from_symbol = f"from {self.from_symbol}" if self.from_symbol else ""
set_to_symbol = f"to {self.to_symbol}" if self.to_symbol else ""
if set_from_symbol and set_to_symbol:
return ["forex", set_from_symbol, set_to_symbol]
return []
@log_start_end(log=logger)
def call_to(self, other_args: List[str]):
"""Process 'to' command."""
@@ -164,8 +175,6 @@ class ForexController(BaseController):
help="Start date of data.",
dest="start_date",
)
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:
if self.to_symbol and self.from_symbol:
@@ -176,6 +185,7 @@ class ForexController(BaseController):
interval=ns_parser.interval,
start_date=ns_parser.start_date.strftime("%Y-%m-%d"),
)
self.data.index.name = "date"
console.print(
f"Loaded historic data from {self.from_symbol} to {self.to_symbol}"
)
@@ -237,6 +247,27 @@ class ForexController(BaseController):
self.queue = self.load_class(OandaController, self.queue)
@log_start_end(log=logger)
def call_ta(self, _):
"""Process ta command"""
from gamestonk_terminal.forex.technical_analysis.ta_controller import (
TechnicalAnalysisController,
)
# TODO: Play with this to get correct usage
if self.to_symbol and self.from_symbol and not self.data.empty:
self.queue = self.load_class(
TechnicalAnalysisController,
ticker=f"{self.from_symbol}/{self.to_symbol}",
data=self.data,
start=self.data.index[0],
interval="",
queue=self.queue,
)
else:
console.print("No currency pair data is loaded. Use 'load' to load data.\n")
# HELP WANTED!
# TODO: Add news and reddit commands back
# behavioural analysis and exploratory data analysis would be useful in the

View File

@@ -0,0 +1,43 @@
"""TA context API."""
import os
from gamestonk_terminal.helper_classes import ModelsNamespace as _models
# flake8: noqa
# pylint: disable=unused-import
# Menu commands
from gamestonk_terminal.common.technical_analysis.overlap_view import view_ma as ma
from gamestonk_terminal.common.technical_analysis.momentum_view import (
display_cci as cci,
)
from gamestonk_terminal.common.technical_analysis.momentum_view import (
display_macd as macd,
)
from gamestonk_terminal.common.technical_analysis.momentum_view import (
display_rsi as rsi,
)
from gamestonk_terminal.common.technical_analysis.momentum_view import (
display_stoch as stoch,
)
from gamestonk_terminal.common.technical_analysis.momentum_view import (
display_fisher as fisher,
)
from gamestonk_terminal.common.technical_analysis.momentum_view import display_cg as cg
from gamestonk_terminal.common.technical_analysis.trend_indicators_view import (
display_adx as adx,
)
from gamestonk_terminal.common.technical_analysis.trend_indicators_view import (
display_aroon as aroon,
)
from gamestonk_terminal.common.technical_analysis.volatility_view import (
display_bbands as bbands,
)
from gamestonk_terminal.common.technical_analysis.volatility_view import (
display_donchian as donchian,
)
from gamestonk_terminal.common.technical_analysis.custom_indicators_view import (
fibonacci_retracement as fib,
)
# Models
models = _models(os.path.abspath(os.path.dirname(__file__)))

View File

@@ -0,0 +1,927 @@
"""Crypto Technical Analysis Controller Module"""
__docformat__ = "numpy"
# pylint:disable=too-many-lines
import argparse
import logging
from datetime import datetime
from typing import List
import pandas as pd
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.common.technical_analysis import (
custom_indicators_view,
momentum_view,
overlap_view,
trend_indicators_view,
volatility_view,
volume_view,
)
from gamestonk_terminal.decorators import log_start_end
from gamestonk_terminal.helper_funcs import (
EXPORT_BOTH_RAW_DATA_AND_FIGURES,
check_positive,
check_positive_list,
parse_known_args_and_warn,
valid_date,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.parent_classes import StockBaseController
from gamestonk_terminal.rich_config import console
logger = logging.getLogger(__name__)
class TechnicalAnalysisController(StockBaseController):
"""Technical Analysis Controller class"""
CHOICES_COMMANDS = [
"ema",
"sma",
"zlma",
"cci",
"macd",
"rsi",
"stoch",
"fisher",
"cg",
"adx",
"aroon",
"bbands",
"donchian",
"fib",
]
PATH = "/forex/ta/"
def __init__(
self,
ticker: str,
start: datetime,
interval: str,
data: pd.DataFrame,
queue: List[str] = None,
):
"""Constructor"""
super().__init__(queue)
self.ticker = ticker
self.start = start
self.interval = interval
self.data = data
self.data["Adj Close"] = data["Close"]
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.controller_choices}
self.completer = NestedCompleter.from_nested_dict(choices)
def print_help(self):
"""Print help"""
currency_str = f" {self.ticker} (from {self.start.strftime('%Y-%m-%d')})"
help_text = f"""[cmds]
[param]Currency Pair Loaded: [/param]{currency_str}
[info]Overlap:[/info]
ema exponential moving average
sma simple moving average
zlma zero lag moving average
[info]Momentum:[/info]
cci commodity channel index
macd moving average convergence/divergence
rsi relative strength index
stoch stochastic oscillator
fisher fisher transform
cg centre of gravity
[info]Trend:[/info]
adx average directional movement index
aroon aroon indicator
[info]Volatility:[/info]
bbands bollinger bands
donchian donchian channels
[info]Custom:[/info]
fib fibonacci retracement[/cmds]
"""
console.print(text=help_text, menu="Forex - Technical Analysis")
def custom_reset(self):
"""Class specific component of reset command"""
if self.ticker:
from_symbol, to_symbol = self.ticker.split("/")
return ["forex", f"from {from_symbol}", f"to {to_symbol}", "load", "ta"]
return []
# TODO: Go through all models and make sure all needed columns are in dfs.
# This todo is a duplicated in crypto/ta_controller
@log_start_end(log=logger)
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,
series=self.data["Close"],
length=ns_parser.n_length,
offset=ns_parser.n_offset,
export=ns_parser.export,
)
@log_start_end(log=logger)
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,
series=self.data["Close"],
length=ns_parser.n_length,
offset=ns_parser.n_offset,
export=ns_parser.export,
)
@log_start_end(log=logger)
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,
series=self.data["Close"],
length=ns_parser.n_length,
offset=ns_parser.n_offset,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_cci(
s_ticker=self.ticker,
ohlc=self.data,
length=ns_parser.n_length,
scalar=ns_parser.n_scalar,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_macd(
s_ticker=self.ticker,
series=self.data["Adj Close"],
n_fast=ns_parser.n_fast,
n_slow=ns_parser.n_slow,
n_signal=ns_parser.n_signal,
export=ns_parser.export,
)
@log_start_end(log=logger)
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",
)
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:
momentum_view.display_rsi(
s_ticker=self.ticker,
series=self.data["Adj Close"],
length=ns_parser.n_length,
scalar=ns_parser.n_scalar,
drift=ns_parser.n_drift,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_stoch(
s_ticker=self.ticker,
ohlc=self.data,
fastkperiod=ns_parser.n_fastkperiod,
slowdperiod=ns_parser.n_slowdperiod,
slowkperiod=ns_parser.n_slowkperiod,
export=ns_parser.export,
)
@log_start_end(log=logger)
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",
)
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:
momentum_view.display_fisher(
s_ticker=self.ticker,
ohlc=self.data,
length=ns_parser.n_length,
export=ns_parser.export,
)
@log_start_end(log=logger)
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",
)
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:
momentum_view.display_cg(
s_ticker=self.ticker,
series=self.data["Adj Close"],
length=ns_parser.n_length,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_adx(
s_ticker=self.ticker,
ohlc=self.data,
length=ns_parser.n_length,
scalar=ns_parser.n_scalar,
drift=ns_parser.n_drift,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_aroon(
s_ticker=self.ticker,
ohlc=self.data,
length=ns_parser.n_length,
scalar=ns_parser.n_scalar,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_bbands(
ticker=self.ticker,
ohlc=self.data,
length=ns_parser.n_length,
n_std=ns_parser.n_std,
mamode=ns_parser.s_mamode,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_donchian(
ticker=self.ticker,
ohlc=self.data,
upper_length=ns_parser.n_length_upper,
lower_length=ns_parser.n_length_lower,
export=ns_parser.export,
)
@log_start_end(log=logger)
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 data",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser:
volume_view.display_ad(
s_ticker=self.ticker,
ohlc=self.data,
use_open=ns_parser.b_use_open,
export=ns_parser.export,
)
@log_start_end(log=logger)
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.display_obv(
s_ticker=self.ticker,
ohlc=self.data,
export=ns_parser.export,
)
@log_start_end(log=logger)
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,
ohlc=self.data,
period=ns_parser.period,
start_date=ns_parser.start,
end_date=ns_parser.end,
export=ns_parser.export,
)

20
scripts/test_forex_ta.gst vendored Normal file
View File

@@ -0,0 +1,20 @@
forex
from EUR
to USD
load
ta
ema
sma
zlma
cci
macd
rsi
stoch
fisher
cg
adx
aroon
bbands
donchian
fib
exit

View File

@@ -0,0 +1,525 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.27.1
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&from_symbol=CHF&function=FX_DAILY&outputsize=full&to_symbol=USD
response:
body:
string: !!binary |
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
biSQNvqmFy1KuWl6u1gtXrn7QWrWF/HYoub5/Dtt9E2PeuylRsP2S0gPI93Z35dFecfM+7+skdr2
B5UIl87Uld4cRFr2UkWkYNJpbU8ymmZDh+Lbl0X5nbOcRNqYPFCMugtBHzMSjTOV7TuLDvrTLPDT
8KuhSX7qjC+Zm6b4adifxinP775pxkWksihvGXnfyxqpuVRRhbTrTP2OxGru5Acp/Wlwa1NqD1p4
E1YWb3/tT+O/62JlIpZe6vUnzFDiXHOpTjcy+IJQLPtRL6RzbfvvH3MJKblUsBcdqTP1jplInim5
VBTPtIaQMu6/zAFIDyOVlwr6U8+cRfFMh7/TQaTnc6RbCSm5VLAmHRP8NNzje39ja6TpuE9+ql2d
4Zp0HPxOw56fXEqdsw7SawfSRm7Kvec+U+9Ciku339D0Gqm65h3WFzxTc6mURd1x7Q+ktihk0Z1u
ZLCCluqdhDsSmYqmzk2DrzOkdCjh904y5Pmz4U+T/lQxamt4qUNI7U/LSHX7njl7H/sSqbuRQa1k
DJ2pNb3B79TdyLtY+S9M+i907BPprhjl3PQutfhEiqpkWCn7mkkF0vF8J0KWPL+nOWKKSzVy090Z
n2ooMZ/7U3cjs+j5pexK70LKfR33o9GNTPajUh3e9DacBJOORjcyXOtL+dPGJBfyqOhk0Ttz04Mx
imca4xBSe/5RU54fL1uHlV0xUkid8VGFdJe//Il0jy8g3b+RR4mfunMWk9+pOxJ86yilQYuznuZR
0ciic5tPvVT4jcOMKaT2p9yAeTf6+NE7qeeev5HxsS5lpNHoR8mi3I2M4dxU3+nuuI+6VDS6kY2q
5FAe9TrT3wAAAP//gigAAAAA//+03UuS5DgOBNAr8SeSuv/FxiJsajHZ2XpeRs2+LNKLFEAADjj+
3UtdQnq9kJ241md/Wvmd2p9SX6pokqtYXexbg31G6oyvs9JLL+XOrtJoUY6lyPCWLY7PXqp0ef6A
jQzyqHoeSy3FUkkWvRmh6Ez7PM2jSpJF84367fv4gTSIpYg0sH16qXmLOXMNZcjzB2wkZ3nKEHNm
NrJUWlRQQeu8fTFnK/BSiqWcRZc6j2p9X6T3Ya2v3Eluepdj2/d8FDx/uRM28jJ3Uo9jqfLcf/pB
Os/5/XIz6vOZTp6pK2iDGd9Q1OcuefD7H6TjBZaH8an7pOsg0qC745yN9K74bwLzjNS2b90e1qW8
h9e3bzbytz6DnxalM60Bd7KEdJ+faVmM+YP4tAjpegGppuNKc4SyiDTolqvH3XIJI0Gk/fw1tT9N
GF5aVBBLjWMu2jXpAKljqd2OmTOrNQa2H+Sm89yfjvMIxblpwpyV4zcKld4P0vlG/bQfd8pWxqcB
G3mfd8m3cVg//SANOmU5c1aGOrqN9OZ3GsRSvP0u25+B6j1v33X+4lhK2Ym1O8r9WJkoO8j4anfM
X897z+8bSM1G1u3pOE5zOObvl5Du8ynOqvg0UmssQroCtUZOx+3z6bi9hdTzptzD29SHUj0X3SrP
tL8wczZ1+/VoF+d/kQbzUffx1JGn4+qkRQW6kpXfqd6oYDIWuelOdHoZn9qiggnu5wmZsoMsunFG
oqmr8zch37+b4P4gDbYyMUJhb89dzm3fbGSggic+qjUrt/QppMFuY6o3qH7azEe1JdsPdnFaq1M9
aAnSwTO1Tu+yWqMUhkag1qgzTRSG6E9vKQu6eu7bDxSGLn2nTd3HiU7vlJcKdHq5SeA3kbsfetL+
Ti99p4HCEPOoXqTZ5f7TNhT1BVm0/SknuB1JN0bSARvpWh/rp3egiEGLqueTB85N2+nUUVlBT+9v
vNhPpGIjvZcHkwcfpEGt7z7ulVzulbw3kAY9vdP9py/MnCGSXhFvuo9ZnhGw5kVIrxcUMchGBvw+
qj0r4U0Ha9LS7DpXxPggDRSGyEhIT7p2V3rHLaTtXAvpNwWqH1m0c9O1gDTZzGI9FNVQrkBlZApp
0NPrM5USlvtQKj1/oDBk7oRs5D5Vb/ggNXdy2fav81meytsP/CnffWXRv9FA//BSPNP2wmTspMbM
OvengcJQdceMmDPXT789V89IywudXeaig+9U/jTo6b3O+vrS+FTvftDTu2lRVBoIYilaVHkllpJF
eYL7O4X1jNSxFDdbV00b/yZS9Hc6aB+kjqVY66tznkd9nbffXnj3/UYFUR9tvwY7DlWZqGTN++m7
PwM28rfi508+SlVJd3Y1dCLMhI3k7Tepidcd7OKsQuoK2kXuZNbzqqRv34wE+aheyUgYKbzUTLaG
ciq+qWPmN7HXf2h0E6ljKdZP2yBrXk5Znhn19Fqndx+/pupEmMpNv9JgnOZ43M2RzkjIouobim2P
U5zfWZ5GbTn0Ss6kp9dTR49aSF8YnpGo8qfBbCRjfk9yBVOc6JKfSU/v6OdzfMfTHDPJTa0xc1/n
SIe8VNDT6yy6rPPJg8UzDeaj3NfXj/v66rOedJnJbKQzPvb1eRdnrXpNA4Uh66Gw+7gH6rdLSAPt
jn7czz+CqfgtpOMNlREiDbQ7ZPvITV/SlA1URdF7fomN/PyZ+sjx/ctj+b9I0df3DQ2GkBYj7ULa
MG28oSn7/Y0OpM1R33Nf3+dfDM1FFyNFre9SFv35jeda33fseehMl8+USBeRLiLdivrw7n//t0VI
B5E+c9F/FJYfkV5WaabtN3upwjNdvH3b/pTtN2p0702kHXnUXv5Oyy2kgT9tQirl671o+9/zeEZK
f7qfe3u+SLeQDnupC0jrMtKLSPv5maIT4RIbGb5RlyyqGKm+U7CRX6R+o9q57Vd9p9X+dNCihnQm
SjFSeanaX/BSQxYV1FAGLcq2P4R030Lab3upCqSFsdSi7e+Fd39t2/6QRRXGUrvo3d96o5Ztf916
94v9aetEighl2fPvQqT0p7vzTBVJ78JYajM+dca3GUv5TPcyUmUn3neyG5Eqj1o3I+ldFUmX6jM9
tv2NvTzfe3lGOoLcdBdaVJs6U98+4tMR5KbBmWqHzIJ6w/evLCGd9vybSBFJr+k3qkwhvYx0CWnZ
8qfNSJuQjheQ8o3ajk+LLMq56R60qKHbX28gtT+9FEnvayqWsu0P3b7ZyCDqm4r6ht/9SxZV9wu5
aeGZHkd9I8hNg/qp2Mi9/ZqCNx1JbhrU+qSBWF2XgsrISHLTVY8raMO56cXb9+44bmYpRVXJ6dx0
yUtV16UWs+i9z8/0eX/UB6nr/ORNSxk6U2fRmDkbSadssI9PZ+oNQqXIopybBnzUIxv5/QlrIRXd
frHtk4suRdWebS9180zHG5sD51nHzJ8GkGek/QWlVm4N9YxEgXrDSOZNy3V8pgFvWpWdBPOm7piZ
/f9uUf2dfSfsPQ8mY6Eq2pN9J1a+5qarQPf8KkK6XpiRoPptCyYPmpDOF3omdjnvmUBnV092yFzW
6lRXZ6AojBmJnmwNXe7uoEXNF870DV1JdssFCpibSN2D5k1XjPoCVVHwpj3p6bXydWWnbDn3/M6i
77XOs5Pj3p6eZHzXZnaiLqTRT2P+nrCRk1w0dx64hnLT9s1GBl1Iqp7ftZ1mfD3Iovft7g6x5ttn
2vidBt0dRNr2cR+K+P2esJF7HHfLrYDfJ1JX0IbrUuzt6acdMz1gI+9BLzXppVztGTxTx1LcJFBq
OUd6yUslO2SYR1VuEblOZyR6kvEFO7n47q/Tak9Lujqp1Po8d/LnUT/bd9KSrk5Xe7ryfbM8N7rk
W8BG3t4hQ4sKuuXQ3dECNvK3ssLfWlTwmkIJqwXMWfCd+kzraa2vJV2d1D0vRbbvNyr4Tl093/an
88yfBnN8LcmjhicPhLQbKd79luRR47hP+h5GimpPS/Kowajv0ndaHPUN2X6QR5V2ijSZ5hjyp0Ee
1X2m13mnLCKUlrCRl5Fy7sReqstLBbORxf50nuf7l2w/YM68i7MolpqePNi6/SCPWrx99fTe0+/+
ku0HeZTZSL7707a/9J0GeVQ9502DuRPk+y3Jozq5k6FpY+v0fvnsZ6S2faqMPG+4DHfG9mekNVFq
pWKbVUUD7gTvfk2UWr3dds7z24c/rQkb6Zr0TaWB+5Tjqwkb6U2s3BqazO9fQhpsC7fOxDxneGcX
UnspKrUWKgtWT8V3WVSw4bLx3b+k0xvsiketryZsZFCXkj893sD+QRr0THhXvGzfOmjqQ6mJGs6v
/P5/AAAA//+CuRQAAAD//7zdW67gKBAD0C0BRR7sf2OjvvmbvsrxiGh2YIVQ1MNlZ/G03OsrnX6g
hsNcqss/KlFrPHT6gVIrc6nfhAP/pSoaeHLpjQqUWm+6MklXMtKUVYZihaFuxwu6hwWqojf/0/6B
Ti9dmaaVWg8ibV+4iOj0p9UajwtIA4dLuzJJXypxZeK7H/idsN4fUhjqdg3tt6KUFYYC30grYNrn
rOmNShwu7RqqeBp4cwzdqECp1b6RZCBasS1A+oF33GjkSwX6Ul1IA+Vru9xt9qV+vsfL3f9jHhVU
0YHuuSq+xDP2jYP2gzRwuKSa+JCHTO+Bk8AQUmd903rSqk4CH4k3nvSDNPA2Jq9PqqK/jdX/qqOa
kB4feB4c93YPpb8xZh6kga6kveL3ddB+9OnekX4Rperer6PetOUepIE+P50ETvb6nJ8OxdMedNDM
6VXFV8dWV/JBen/hyqQodawtRveDNNCUNfe87/dQTiKdH7Dk2UGzh8xrn/9BWh/0T+kd5xv12pN+
kI5977h2U1O2bfVPH6SBpqwjv3J+uzIF/2mgKct4WrpRK/DjO4A00JQ9+e63fWeWduvdDzRl2ent
crhsLTh93ahkbspppHjSawU9ab1RCf+UHLSTc9M9ruSDNNCU5TctsZDW2o380TSSOX+bVL/d2438
QRpMI63Qznd/+D+tS0gDfX6e/sV90777RvWkNvVW/PhgM7Z3IQ3iqf/T+QFnYghp7Vcn79vGPzCC
KNWEdPwf7I7gRh1TSL+YRl7tg9qUSAO/E37Te+3np/ME0sTvxN5x3OL0jO9YQnpubsdFDMSAKbv4
Tc3pHdfuftTqgZq4XtOA02ukVMJa5vMPftNAqXVftSlgIJbiacDpLWvKiidtTdl7CamVWn8j4/9X
7nn36U/lUgmnl0iXWJ3DfP7Sfxpwek8rt5AnHXB6DyEN1MXI57+4zbGnL/UgdTw91/Y2xzCn91Am
baXWZa+j4hankZ7KpFuwF23uub5poC2HTm9L/E6sJi7Hi9W8FY/JWUv8Tux4wSgV3ChkfS2pos2V
HMf+Ht/dhPT8QFuOPZSr79ZRLeL0ujalc6Az6TaEdH5Q8R3X/kRi3EJaH/SlOOMbweSshNRVdPmb
qidtH171pVoyjSzOozjf7wFTVlEqmEZORqnr3mfJT939hNNrj0MqYQX1/iGk5wccNM6jAk2E1YX0
+MI5UNPIO+BMKEoF00gzuunGeDqe8jUNOL2NSEt9qctdybWE1B008vp6V+Q/ze1pQhpoyjrnv6/9
/ahbb5Q5vckml3goR9/aj3qQBnff3/SDeHopSgXTyPva3uY4g1k0kZrTy8lZ59w0QLoUpQJOr5GS
JV/mn57KpANOL/VQutQa+xybSNsKppGD+enQ6fdrj8//IGUuVTz9OjHhHQGfv0pITyPV3S9F/jIT
od67PX+QMp4W51F14T8tT87qfcrzBynj6Rx696eYsr/9QH990yak3UiVn07NTacnvPM9P/2DlLlU
lV7TOZD11biN9AJS16Z1+T+9daP4RhXvvmvTYnUytXVUx/B/qhvl2rSKSJWhlJWv6+bpT8fTydNX
PPXUvGoKKXOp4nZcaZtjmNlVh+6+a9PBnL86Tn8E/2k7hZTxdBzc4xP7eDiXGheROp5yL7qGvql1
zwdf0xbkUkQqxkxZE6G6MhQzZQc3uJ31iYfyDwAAAP//rJ1bkighCEO3JCJo739jt3rmzs88PKmC
HaRaW5GQ5H/FdkcaHWrjQ6uPSOfB1efzFJ1a3eA8nfyOmkk1/xDOU1z9GXSeCvv0vvpH0EY66k48
sOa3BqSsikenVj6lhD/qzpyNI7CR0+nkd9LwTmZ5fCBSQRWPdxTd+8ZT8nMhUlbFJ+qijToTrJG4
quI/kbIqHlffaFbyN2HKDw0vflPuoOHr5Dc1+jddNCu4/RBSoYOGWh5ywlL6UhtXX8g7wV4f8qZH
8O4IQjoa8qOwey5kyNyZs3EUNpInETLqDO+ahDQ71BxeZ3gn3aYCGxk1h3aR37+rOV6kzEgkq42p
z7+EmQk6pRS9aZ3fV5KukpCOBncxVB4okwj0RwlsJGp476lM6uo/hFSY7hgl5YE425O0TwW9KfZP
R+IcyinOobxIsyOPj2YmBFW8I1LvWP1TV8gEnadCwiVOIvAd5cLqUy0l+PQ+9ZOfZyaAix5bcRhC
DS9rzYvOgp9IR91R2OYsr77dtTxjSwmXXPXtetUHlfRWVJyHXUaoPhVyI6HXt6WES04NpX3Kvb6P
rXxHKlR9XJ+id8es3vtbmekVppDQEYPvfXjvbynhkv1QKI9PSAtP+qbCTC/O9Q3K4/stiuLH7Dmd
p0IyS0PWkXBHDfymwis6SjW/oN//Cr++I81ijoS0+skTiBO/KTtiJOpNDykOZy1H4hOpMM+PnYnA
xAtWHG6qpQSHIcFdzOuV9EKko2OqE1Xxqzh/Orbi0zt49nw2THQvQpp1pwGjORRlpvehP0qY6d11
T4QleCHRvS/M9O4o/1FKheKEVHAZ4bdpQyXNf5SgkcDVf7J+SsGkbCraSHbAREdhIfHiPif9Ij0N
fX706lScsPCb7o4pedScPUXF4Yu0Qx+FJ7+gkDlJSKPBqZW8kIxVnAbcSSraSHzv26R/fzNSc0LK
tZRz9xzzTp7qvZ+S+y2+TbOBj3Lap8LblDu97NQqOAwh0tOQHYeeXYIuetHqC2ykoDb2+j71IKQ8
3YGMhDHLI2TH0b/PbKRw8mdDhkzQbcpvU3NUcRIbaaw8+OjA35Faw+rTKaUkCTjd+8xGGmp4J538
xn+UBVV9Qt4J6vc5mYX9+e2h85TfpoaMhNEcihlPdjlVKCMa+Cj8o4QkgUCkfJ4OPk+tjvSeefAi
FVJEoj7ZJSSz3PdpKGwkp91srKQZKUzMhMJGHk5ko1oq+Y+C7nkor2juS2HWkQlcNCIVMg94Bu3U
+X2Y7AqFjTzcl0Lfc676HkS6Glhz7EoqvCmuvsBG4h2FDu3K6yQIqZAa+pTfUYKXPHAnobyi+R3l
6Cft1Zo/FDby4Dtqkev9zmoHLYiN/ECKq2/ERSf79R3apwIbeZjh9do3/dogd6TRwUWTt1wwx3eo
QhF8etlfahEbyZ6yxEUHvfiY4f1jEPbbNxVW3wgpuzWym/ig1RfcGmEOJRSfXsH9FpHyN4W+VCg+
vZllpDGKvpIvUj5PHy//+y2rvxqSrjCZZfMp9VAtJbCRqI5jB8xHSAvHbyrUUqfUPf86LEt5Jy/S
Bn0Uc9HClDz0+pbwNjXjSdldZnmIN12KNhL3qU16R20h3Za+qcBG4tvUDnYmODEY9ukS3qZzNOhN
GSnMS/0DAAD//6zdW5LsOAgE0C1ZEiB7/xub6K6vqXb4cKNqB1gSmEeSGY3adHJuOsUwdLeQ+r4Z
+6wh82Mp4+kMbxtrf9/90zlpqXfN2Zea0o2cYaaB1O03GIaoIrKCPBPe4GaUajAMsc+/itwd5piZ
IUsZT2Mok44ldjHvR63gmZqxbZMLSYiZtdmVDLA3RIdhiP/9UDy9o3b5w9rEM2UuFWRpDnV6lxHd
66Lvm7Et5FGh+X4MZigRiqdGygYnZyEupFh+p6nbN1I2+DcNRakwH0qkPMrTyMW+VCy+U3pUzJSl
zKWCudTdM3yz1PH0VOQ/GmyN9ii9U+enwVzKtWlQlyc05WmxNfKdOp76HyXujjCiO7B1tBoVXyNK
aZMrnPMH+qerMY2M4O1rah5GHwemPKsxjUzu76d2zWM1/lGnLGXWl5ycpW4/jUDM4pmWz3TQUpxp
+kwTk7PVqPiSyiwpPH/6neazwuWPpYxSSWWWLFR8aaxkYn9/dXh6ucOboQzFDJi5ePuMp8m+VGrX
PLwhEynfdxUdnEfl0pl6PyoPWmrfP/hOVe+H66i49Dc1pjf3+WmUCnPKNs6UWV9yblqHfN/dngTP
xGpU0ckOWon7ON2Z+D2PZ0sdTw/evhCIaXRHht6pK76kWniqjgrj+n4/5tlS51IXb38Mnan/pkO3
b0xvkb2h1OdPqzLlxTNlPC1uyJRQSOUeSpUyFFfRtW0polQZe15LGYorvuKEdx+ooutonKnqKE8j
kz2UuyN7y099pkAfz0YdVaz4KhVPPY3UP2o26qhiFV3il+p41LNq6I+l9n32ULbQx+X/fqHXNxt1
1Cae35aWLd201L5Pnok9D0UpVtF78PbH5/F0H7LUOOkKehTrqM16f2vvpMyHUmC/nY06apMRY6vX
t43u2Kl46jrqJEvznWj9/y21etimR7mO2lRlOlNnmsylziWPch1V1JDZYmyratx+ytLlKMUMRf/9
cgetTnrU/EJ+Kh608uSssHE4G3VU0fdLvt/KT+VRnTqKWZ+mkWmPqnPKUudS5Orc2uZIszfkpXfq
OmpzcralxNr5m2K+Pxt11OaEd5feqXfO9qRH0ff35b8pznS7h7IXb3/49s/PLfWZYkdiNHYj72LQ
++0r6zunz3TI0sN11PmxR5kBs7AjMRrTyLpoqbSNK65P66jRqfiOg/991abuoNWl2zf+NImU3cJK
pncjfzvwz5YaM8HJWYlfqoHtyT1lqTFojKd3QIM3DJpn0YvvdNpSIhE0OVuu+BbfaWNyNo3skm6k
c/61FaU6+FOikMSEtaxrLsTMaOFPD1qqMw1rcoU8qoM/Japz0dIG/pS3n1/QjNXsZE3jT7fiaUPh
khwzK5ZU7hqqoSVLrXDJvei5xdxyWpMLM77RwJ+OUpSacX6uHjZkqavoyUx6LlpqbrmSR7mKnkQf
3ymgvm1zMPJPaHOMjsIlETNTqM6OpaV4avzpZBU9hZbraMUzkzb+dJ5WDdXtm7encfvz89tf4uqc
xp/Ok5aOLyixUjN2NdQYH27/h0q8o3DJDGUKfzrH8VHkf1lqHjRuxc8lxjZ3eudBSxtbnGYYEk+v
WUbG01b8y1JvcQ690yE8/x21y58tzkOWmmGImrFDisGHFS+Gb39+YdvYlppZ8Akt97LU7GLr83fa
ULh8QiG9LHXWF+ZBk++bs2ssWeoqeizzSmqH16xNg/G0wSlr7mMpBt8JkP2xVL7f4JS9eKZkaXaf
/5cJ/NnS+AL/qXrSd1Jp/xxPGwqXnEcNzaLvRLv+hQHzZekXVO6GML2Ht+OOk75vRgyikP5ydP8H
AAD//7zdS47rMAxE0S1JpGzJ+9/YQzqzdODDhow3z6DgWDTFz61fSk0Xmzr7BYdLM7rJPi6w5e7u
pm+l8wG/ky6a+Nij4L2VFvj85/7X1CfqzunqrdRM2UF2h7aNK0Th0NkvMGUL7OOpnN/sjjt6w1tp
gSmbvPHtO7DHuFda6UYWHNgX2cfOT48Bpb6bdpNaNX/ao0C+Tin13XTZ1V61Ps+f9jOkdJqGw/dU
81LdnIm+Dikd+880dI/6Bpz+dd/ne2o+P3snjlLe5OrrlNLY9znrdLpaBRbSlNL+hFc8PbkKfidN
Sk2+vuwho6zv2KM0/yjtD/jw9qB7WO5mKJVu5DcQ/F/rUkfBg1vv6SN3U/qdFFjyTfHUd1N7xbsu
VXC8aPpGFe6m9pCxg1DBK16Rvz/hdTTJQJz7SgukVkapb+DAD1bnntfRW2nBh9c8aSld5p9eOvu+
m367An/yT8f+M23K+ZvJ19w3/Ybi/GB0F2ji+ppWSK37POlpnvRaUhoP8KTH3H+mzE8LpNZglLqt
n/6wjwvexvf/fivM9DZWz9tBJwF7c6Ay0Sp+J80uzMpPW8HbWM80HolSojR74/CHSX2v9HzA7Yb5
qac7FKVaye+ESkUZaYXZHvT4WuUWXaj1KZfyZNct+/it1Ldo0hof8Y47QkrNlKVvZKjS+60U/Evp
CaWFbiR9eTiF1H07iZFS6hm07mk5/fv+mvZ1SakraNzfD32j+uWJmdB76lt0Ya5varIrc3e2pxVu
fMHKRGqDuzIvdSieFmZ6uc0R2o2MLMz16WtamOl1R0K1vvA2R8wupY6nre/2TqIV/n3F08pMLyO/
+Pzd/X3VpFuhGxnkTEQqSnmbI5oy6cpMr79RoX/fSrvOfmGmt3D25384+4WZ3oNOApN8/rF/9j3T
W6Dea7YnTBaM++p5uyqTsqxLpeb5w/umGV1K1/5EdzZ6HnjrqPGZMpdKRqnsPFGFiW4q9X4UeX1J
xwvX+vI+Sr2UOp5edmdQ1mcmQt5PH7+UxgNK6XhxWWk/pJS5VJJ/mqpMfEtxf52oJqXNLHlyz0Vt
Cs/0ZtN7WvA7oc8ZlaanOvPSM/XddHBK3i4iBer9dUqpdyNZ5/8GDP6r48XoiqfuRianOlP9/YqH
zH2X56XUfP5udwY9U2coeT9/+lJqPj/npYam5L+tq/9lh/et1LTGg5TmQ+4Mw/v7p5QWCEMkYJrU
2s0/XfqauhtZoImTfG0C5hGKp76bDu+aX6KJVzxkLim13wmZXUOzkmny9eDZL/idsIYyRGxL75yN
VDx1N3LwRA3V+fMwZ+J+tqetQjfS3yj78ni6Y+BErYrfyWTkX8yl/ExXh1Lfog/On56NngdUOuaQ
Umd9pz0P5HYzzOi+n5Z7KXXWxwraUJcn3Y8a9zsSL6XO+kjAzIvkFmfSiPyrQhgK3vgYpZqVnjr7
BcIQ56RT+/vpDCVjSWl/4D3tqXjqCtrSe1ogDLGCNlTnr7ynp6KUu5HJrG+IiZCtcDuhUnckputS
pOGc+2fft+jwidIeXwz3TZfOfuEWTXfbb6S0j6pkQSlPlM8+JxFGktjm/LTxPW37TqypDm+6Jp1L
8bTQjaTP2WDXvODEOpWhFAhDhc7ZuV/nn3ymnu7wLg9pOM5P0Tl7KfXZ576p+1HOT2OGlOYTnTP1
Tq4CB03xtNCNpMtdshdtB/ZsfKaOp2R1Jm983ubI+93INiu3aFKaU7vmcTmeojIxCze+JFsu2TX/
/ME/AAAA///C1pLGjH0AAAAA//+s3dttxTAMA9CVoviVu/9iRX+LIocX9gaEbCQmKVF/kK4DO7mc
1mikkzW11se5k3ErU9ZJA6NY03Fgw6XuaQ8UtA9r2g/sN+UuTmcijPc+lF+kdWDHIZVeO7xjfoTU
ChqnOcZN/bSs9jxAGuT0el80a5ooaCWkz4G95uqXSlKaefoBi/ZLusSiPW8qZWIljI+ORNO2m3YF
6jnvadtXJhpPP9Cl2hTSe5/x9aLD23eViZWwaE5ztOfZd82X/lEJi+ZLWtsYExY9LiANfFNOHVmV
DPwofqWuuV9TIw1c84tI7UgwvYEv6VbBPRU7CVj02lbQope03lJm0f12Z5c6Zpwu1t7Txa6ZuJE8
/a4MxP8u8pe+6QzcyM7c8wDp2O1DmYkbGXQhTZ2+veiaQnpAQevslQz2mhdr6u9pufdc6nkwI/Gq
Sv4AAAD//7TdSW7dQAwE0BsFbPbwW/e/WCDt4th6DKTsDaN+D1QXh6oLaUGjm5mJzryU735w9/ON
/KmyPXa7yc01bc9nzlLqYjkKSFNI44WJww8nuebD+ahYBW6aH3739ZLO/nk4cXgiXc+9Ob47yf9c
48MLZVWqkQdf0tGf107uNRBPpH5L8WvKnol0n3QeXFNzU04b96H3aZjxNZ7TfGFN5R3XrYWkqaNV
qEYWOmaUk/5uHPmvNSXSQp6fORT2STdz03uVkROp81K++6xFWwGzL0X+AjctdHbpnFoRQ9Mcq8JN
PXmw++NuuWs05R5pYT6KUergLI+j1Ecv6UI1kgpDjFIFpHncv/pmoRqZVBkpVM0f3/1ZYNHZrCgs
VdHCrPkQ0sJk7LTOhCK/e9AS3HQWGJ87Znprj3c/F5H61cd6VJembK7CBDd331Px9uWRr3nm01nz
E6lZ9GLkj/nf2ckssWjG07Ye90qqt2dWGF+YRUu35yjs/hDSgsIQVUao2lTolQzdKFcjm7mpatHN
9f1EBm0WWHR0vfmtftsLSlg8p5NqjfsFXcmCUqvufrOuJPulIqTXB3ZyrQdvVCPS+6n48y9ufXjj
17HR3XH+j+TuB5HmIaRTux9e09Dum0UH/fgiiXQRadeamkUX1BqbPGSioCtJpPY76UQqnYmKHx/P
qauR1pNuQ7qS7umFJ9eJtL/gJKC5k3DdFArtJ9J8QVN2S0veKnix9TUt6PSyAzGUP41e0JS9z5+O
QjUy7CET4/E5jZFCavVbTnGG8vzhWXPonp9I1wuOF6ryVHYftehRUb9lPDXSXC8gdTxlNTL2C4rC
iKej4ncSn8fv02O9gDRe0JOe+4XvfgNSc9PgjEQMaR+HI38SqT0PHPlHx+4XvqaYkRjiptfLkS/p
LdX7bncG9J4PVSOvBQkhvVUVvZBOI9WNQjXyB8OCf13T9JoOrqm56b1a4/Vb5CMRBScBRf4CN22D
3LQJqX0kkkjJTffBGyUXkV3wO4Gy4FA18gc2//VGKTMBnd4Cix4Vv5P73vPrjGn30+eU79NwXurj
eCpvjvSaTq6p4+n9/H4JacFFZHNNC/GUealDb6npG4X5/a5q5A9GRV9zffzuGylq0b3id1J4S9GV
yRm0I4HU1cj4kPFpOi7ce663VK+4htrxYr3gbI2O7l5g0Y1dSG2rItEKeaklpOM5i/4OyBcWbR4F
ReFeqEZWuCk9Ds2jFtfUGbRlD27m+uxzthWlKiza2R59ozxvWlhTx1NOc0QnN3VmAvOmveIayonD
kFZnuLsjQlGq+dVHNZwY2v3tu9+nkE6/+f+8Dr8BAAD//7zda47kNgwE4CuJkm1S979YsN0YIAg2
/hjYm/+DnoJkvool8nc7Dtk5cy61laE0quhwdbJVnRjpkO27iq77edKfcr6E1Jl08Dt1xed6P1BF
F7Q9n/+i79RbQ+tgzj+Q9dXKF5CSQatL9X7J8xf0/J8/0e27ii5WJ7VRm5Zr02KMalTRQQZt8js1
0lSMcsXn299zCWnD9pXzN7aGDvrTQG1aZ7xw+65NF/3pwTNt8FL3Fd9UHdXToYg/7ehQMGlgqo76
YeGgQ3kh7kODNlVH/Qv99U8GbYrrW0a6hLQR9+mlDvKnZnpXCOk0UkbTSdtnR6I2LSoe7jb+2fZ7
/53aS+0tpFZ25fGclTQvdRWQNuoo6/qUSe/L3+kl22/UUe7vL/Gn1kmrbzo7qk7HqFO5VDpGLZ6p
/eliR+LK56pO1Kaz04283ONTl2c2oiktarzRO6ln3+lPp+gWaUPVyU2s44pn6uNGR2I2VJ0NL8Ue
X0eHotv320jv4B7s8njTlb2U30aOpPZcM2UbKvlRPNP1BlKx5zOeKrtmR9Vpnl/TcMbp27fth3XS
g+y5+NNjPj3TaLyNHJwzEYtaSd5+gOuLTjcy7E/VN230TsD2RKfi45a7MZWfXu5Goo6Kjv6Um1mc
9aU5FGy5C+lPfzQCONPzGdIfkeI90uOFrvnKF+L+FNKGZqL+NNJG1zw6FZ9z/lrPmYmURcU2h8La
9BQnnfW0OomO/jTJ9tR+rkKailHWnxYnYY0Rz7s8tChX0b8jQP5r7+Qyh1LyUvG/MGiN7zR4pmbQ
zucvDg93JDZtnwxarWCXB7afDa7vpO27G0mtZJE9n/5Ol77TRjdyipUsZX1lTW9h9nE0OmcNpnfI
osK3H8pPrT/NoueXEiEbuj5sto6G/jSXvFTSooJxP0sxyt3IJIOW6vBeye80l7yUu5HFzlmpI5En
89O823L3awiaKr5fv2HNxEDOn41e9F1t+kE66aWSWV9uIQ0izTvNxBdpvYBUb86uog4l795Ff5Gm
bV/faS7Zvl/Ff87jHik9fx5Eeur2p5Eunqm91EEvdW4hDSNNIZ2+fWUoWYpRy/70IFJmfRmK+6n3
UTnyhTO1P2V+mscp22eGknevjj5IXUU3kFbIn+5H0fSL1LY/5lOLurbP9E6H8kXqrG8T6UbOf1mB
mEP+1H3Tcn9fWslsxKitaNqooi/WUVv609MKxOSZOusLZn3yUuV3fHXJSzUqPnJ9JUV3lvWnUxZl
/emeZCau+UIVzTPNh9rzn+lA9wxaY2qTPH9Df+qpTQcV3Q0VEs/Uen6/NSfSyEcatC/S+QLXZ+25
Laq2kDYYtEF/qpcH0yr5fV+bRqM2LepQqOjOvW37AaSuTeui55dipnOmpTN1bVqnddL7z6rkv0ht
+9O3fyiT9u2fS0iPF/jTU6ykpzfUIlLnUsVoOtQ5M4dSiFHRqE13+NWRlLLX+dSfRqM2Leb8fiPR
YHpDt9/oRs7r8UuubMR9+dPGNJx8/n5/NqaM6PYbml4jjUOTBtw3TSL1dLFJdQfn9pg9v51/+kV6
WNm1qeySqnNb1zeJ1Bo0vjYOzZIf3sD+mfd5j5TKruDW0NBsuRhUywU9/2hMvuY+Pk1vCO+PijOF
1JOvT+7kEtLpt+YzlPVZ0zs5V3KmJrR7zsS8ppB66r03XGpGd2N/1PKZeuq9N7F654H3SFynkHrq
/Wmk2h7mKXi3O7m+SBua3sff6fg7wfoXAAAA//+03dmNHTsMBNCURFGtJf/EHmYAf9gY38MHtTOo
llpcikv9BSli/lbo6Q12yob+07ZtpdoDpJWdsrT83NJsddtoOtPKTllua9Q2nALShqp5q/T00p4W
Jg/WC0gd9T3c3DLn/dzJw//0hR3d3imbnjx4jpC6n596J026PJW5E3jTVtiGU4ikBzdheUZiCWlh
p+yxPdWZWpUpgki9XYx6J8ENQ9tnCva8VSZjD5H2fe+jkJ20SsaX9lG6/WUrdWRPrXDZzrj2ppuW
P1CLbgWFy8Juuf5cIy38p46llrfgSeuoFXQkQkitHUc9vp8kYX9HOj3JteSjnPHFZm6qmbPwDsSe
W0itHTfjNouObQ7lyPJb76SHsxNlfG3fZnytlEVbOXDq9q3BHbJShSx6U92WKsyeN/3O1f+OtJ3C
bGQ6i54v6Jp/Zs/bqehGHuvw6j/d1uL8nEV/ITWDdqjCHOKlpvX4zhRSKwYHFS6HlANXQYn1CKkV
g5s1uMVKnoJe9BLSfEGDO4S0oMV5iNQM2iTShwrsVgsP3r51eHn7fXd5U3PSk0idm3JXZx9Eei7Z
83ZKWpz0+8EIxeq2TZbfupGduuZduuaxCrHUENJXdHipGVt4+11IHUsl41MpsHer2jtCiRdiqQJS
v/2U349XKhK8fUd9W1aqUI3cvn35fW+9/xa//ox02+/zRc24j6W23n6hGnmoF50vRCiNtz/vfVRK
izO8Cylp+QtanIykzUwU/L6Rmpfi7fcpv1/RiyZS81LTfp/qDNdId6UaSTXG2PpPh3mpz8zEF1Iz
aFS5swa3NwzF3EBayKJZ4TWDZnXb3nX7hYyPEUqKP+1WEui9CeljpPRRUmLNsD3NIaSM+nIRqVRE
srsPZfBMnUVzX9/osPxpZiI3kdLyD27CGuqYyUIWfYTUGd+YslJD/Gk64xs9hdRR35HlH6qap7Po
76/9jNQM2vHtL92+WUmwPbuSRa/BF6WY3+x5HnnTQhb9ODvJ+x60RaT9DaTq6+vO+B6+/XihHsUz
tdJVf+SjCj29m9XI+dzzUlMRirPoWNwqqv2nlahvy0o5iw72ofRIIXW3HPjTXalGOjthR7etVNCb
FqqRtKddyoE/NTD/v7rpF1LHUu4+nvO+IrGUnTiLzu4a31YkbR/lM4036lGsnPH28/OseVuFjM8c
CuumhYwv2xZSWylqx/1UB/odabpb7hCpe9D49kOVszYe90stITXXl+yVlDdthY3Ci0jN9QV1Ix/9
p81sTx4hNdc3fPva0X22O7v49guTXOxBe9QreXymg0gd9eW+7uk9eTnJ1VYhiw5OyIQ4lOi2Uqid
rEpP7xz38anP9POOmS+kfvuu8XUhHQX+lEjd08vJg64qTzx+UZv/abpLni9KsVRlNvIh0n6fR/U2
7ztlgy+qUJE4129/uBMBzMSqTHGyn/+nRoM/YilnfJ+n476Qum5KtZufko8/eH4zE6G3X+jppTcl
0jjjlu1ZlSz6eN5UtZPuM+2KpQpZ9ONOWd3+KHTM8EWZQct+24XUu3vPEUnPSjWykZnQvGksd3fk
A6SV3JRn2sVLWUfi2zx8RlqwpzzToQhlFnJTnqnt6T63/VKx/aJC/2mhGrldkdCZnkJvzxZSxlKD
OmdDZ5rm+tJIzaDxRY2Z/7zGNwu5aXKOb6hTNrd5KfRKzkJumuw/HYO16GmkA0h/VSP/AwAA//+s
3duN7DgMBNCUaJGy3fkntpj5HPT2qQt3BoRsPYr14IdK6eAe6VDabo5ZrPQ0a+6vv593erfOU2PT
vshFy8PbViFNcU2D85T6U+H9dxLFv2t66ZQ6AkaCPWkpu3q50ptr6r1PDVpLKdvGUb8eq4+Vmo0c
pjaNlF3tbk+f+vqBpnfz3m/t/WH/dLpUaaDpJcuj7vm6g1NKb6kAm5I76UV0sp9qes9kaigVM4ud
Xt+m0p6fiab3YKd3mIkQ+PeFTirI7nB6w/2cj/qcM1E7mRpqV/zB3B7zUej07mRqqKcwS4lQ7vXV
Z1d87QDxOV/qkLqj/EIpsDw7SRi6nd2hLCSz5siX+ql0f2FicGnvW9VZNysNciW5prWfV3qyUp/8
t/e+KrWm98AdtQPEV8yZOOTfr2Bm7EvnacBG0nlwtLjoy6lNL61pwEb63teavgt/+bc8lJ9K/epb
5E62vDwBa/7SeRpoeuk4XFv608AbiW7PTlycfknTGRsoZUe3aaDpZbcnUHb5zV/c+2YkqEPpen2B
O+HXry+saek/DZyxn2dy1U6csZxs3WIk1nk9X1Mjvl5Ob5AzdgeqTp2nZiNn3D9V99w5vQ28vwM2
cohORvz+2B81I3RiFG0vz5b6ePzqm5tran8UHYdeU/v4Bv/pBGxkU4M2yusLen3zecrdT6VBwhBP
/t3PvTzDNb2+kDHDO8p6qfXimp7PvTxWIjj9VhkzE2DTdVCDdt569QU6lK1K1zcS2vWflistfv3j
OWve5xfSxL2mZiRYKf2mK0gaADadAJs2k69nkTW/nzK8k/hN3T03H+VKT1ZqNnKO5/+pT/51qVKz
kUYn2vsJw4u31ATY9J0i4h9nc7RTm3q4pn5LXfr6W+m3yfv01CllbDp0cW7598eMxFyq1Gzk8N4f
MbzT5vfvVqWXK9WO2lWq1F9/a0cZm451KLd2lJWys7WjAmzK+VFzLaETK2Z47wfYlN7IGZ2nVsvN
YqWBsos46pQOxRndgzuqAzayqT62Cik4pcBIdJLTSx/fuz/5zx3lFwrcHJ0kDPE2dV/K6Q09S5UG
ml7q+ZUysoIkLNymnSA+vqUW1zRAfEhq7YCNXOPpYUzEMI4armkw6Yp8lPj9BO9v7f0ARQd5fa/n
PWmk4XSi6XVaY/djbNpVqDRBfHTIbPFR1qD1oVMqYCOpPe/9erz3u7WjzEYe6/WUN63AebD49e03
5auPbuPIa669H/hNnTTAKSIJa86vb28k9VLvckf/rKmTW4CjOmAji/z+oQnsL3clj9FbymxkkZGo
jdnGr5PnaS2tqVF08X1aSsIq3/s12vtG0cFENk4M9ilVfJ8a8SVaya1KPZNr694PNL1djzVoRzCR
TYjPbGQwL/qjpvd3Tf2fNneUlbJ0HFZrxmGgPoaXZwWIL1nTC5W6z1/gTVeiP6UOxbPjnDRQUCKs
JA3H6Q3C+zXXU13fSiZcXp7GyPmmj9MblhDf/4z8/HtKaaq9u5K/YxA/V+pTium39WKlnmqPuTwr
mXB5+o5SEpaZM3kkVoD4irk9Jf9+UimcXCtAfMHelwKxrOzCfNOfSgPEx0qplA1w1KHzNMiUXc7t
YbrY/RTxrUR/enneibI7gqn26J6vKFPW08Puxz0U5UquRH/qyYGcIHQHnV69pYJMWas6NdU+SBeT
CmkliI//6aG+VNlxqMS2FXB8wax48aYJOnnplALi+7mR+/2a/gcAAP//rN1bjiQ7CATQLeG3vf+N
jSpLI4369s2DlPnfqg7hBBsCgn9nzvTmr57kGvxO+Zba7EGL23v/8xPo6rzsoSiFjO/6N7fZyecv
Gqo9C/Omlz1o0yKk6545+/wX5aZr8NW3wUYWZXyf3xhLSCe+04UK2mWPENJtpCGkG0g3POr6L0dI
F5HWTpui1reLT39UIZ1Eet97ftm0yPdtU+ifFmXRl2fz9BfyqN9+4ifSxe+UUerc16Svv9DpxyDS
RqSMUocedehRg0hPZZQKIuVUfIRuU/RM/LXHLdLCeHq6fP8cVXoL4+lBblqURX9+474mfZ2+GAl0
IF72GEI6bFNWJdt4fvrQmCni+K5/02jT9YyPuuyheFpcQWPkP7c9vR/fP+FXH0/f8ZRVydCr77dA
9xPpUpQqCe0OMrxFp+/36dny/UTGx/7TmOJOEjVp+n7wLXWKtTsYT9dT5qyoU/Zy3COP6sqjVn9+
m4bj6eIL5RCp7/3C77Q/r/OHotRp9qhFpOWF2/TQ9336d6qin5H4zC7OBB+1nt+mdz1oX6TxvM5v
1aa5H/FRF9KE+i37T63aNMajOv8XqbU7an9sUzNn5Y7h/SI1b8oKWlFHd5jfL10eleBNyfGZ5TG/
XxqRepMAb9MiTYQY8ah++kVa3/B9qYuNhO8fIX3j1Rcv8KZ3d9SFNMGbdt6mW7fpSWwQ6kKa2MrE
V5+mOCMcT+/m+L5IEzpojzdbx9qPOhG+SB1Pu3dzUAnL3MlW5M9sZuE0B7fa26Mc+ROdssX9py/s
j5p6SyV2cXKWp0xuZfK+kyKkiU7ZxGYWRv7y3KbhChp1z0PV80z/6dJtmuiUpbZcNHUhzUR2osif
6JQdjvzK92eiq1O+n+FN3YGILHq5ZyKG4ql509VUmdgV3+k827yp7qgEb1pUlVxDDG+QOVv9/i1V
lJteSMWdrI7vdG7WUFYNIDUbuaYYibWQ789Ofn9FEVJW0NZW9XyJkVhQxLjsMYU0wUaqgrbHEFKz
5ptIu6MUuegxX2Aju5A6nga56NrU3bEcpZaQhllzdnfIo7aj1C6KUmYjEx0zSx6ViPxTNi2J7o5J
pIr8u7ljZgipuzsO6/zBPhTbFPd+UW76P6XvH0iHeiaOGd7gd2o2cu/H1XP3oVxl63ukZiMH2cho
z/n9U4XUjEST7x9Ox9VEJwKRJro7yJqzV7IkuGjdUXFe8KgjmzZ71FSUsm5PUKk1Br/TY5vqhZLo
6Z3POmWv9DbBRfP0+wu1PtWlErs5greppzgTLA/3mlu94eI97pHWF7aFT2rJuy5V75FGgo38be71
p011+t4gVGoD0sQUJ9UaE9+pK2ixhXS/MHXEzSzt2STXF6knuZYrvdrEmpiRaEVI5wv7ojXFWWZi
s3UX0oRuz+M9vMV8VC08fas1Vuqfas9ZtXpDBb8fCTayWwftIJ7+Jjr7H8U2Ig0jpa5kkwZiQrFt
KPJ7MtYKmF16KD2hJj6GkNL3B1WbhrYyde827usIKV99g5F/SLenu84/Sgip1RrbJFLpSnrD5WDk
T+zi5E6uLkXhNhM7D2jTxG7jx/ui23q2NfRCajayjefK180bL45O3z29CX1+bWeo03sjhyK/2ciE
YhuVrxO36dT7NLGL08rX6kGrzTZtivzOois3sLei07fyde30KE/FU2GoTSFNbBIAdxIJ3Z7KqaOE
qqgnuNutR8VJ5KbVG9kOtTq9PWx2IE1oyg5PxXOLiDURzhRSx1Pm+zz9jNLAOEI63thyp4zPvZL1
Pt//ILWmbFABU12d5SR0JZuQthf28alf6jY3/QMAAP//rN0LysM4DATgK8UPSc79L7b0h4WlW/IJ
2gsEEVuyHqOZ/4SHZ0sdT4+raMXTBndH8J+aU5Yo+UW1m2xE/oClRsruK7+totdl5mtG/vEDXXOy
iU/jT//+x7OlDVUm+v4iS3NDjXHIUiuzMJ42LD3fR/6GMosZhbcYhoxAXFPvfoNhiL2+Jb2TebGH
soaiVINhKKzFKfWwq8GFpCjVULjk6U9VfMOKwTN0+o3aNKwaKkbhMu/5VDx1bTrZQ5n3L7jkaen+
ngmL/3S6M+HX1NPIxdfUvt+wtBT5OwqX7PSO8/27fynyN5RZuMtjhUtzde5nPP91OsosRMm7gzYa
eicXLHXFt6x1JAX2T8/t/7qSU5ZaP4p70SGuzuUotZ4nEi9LmfUl5/spfdPtruS+b1nKrK+W7mkt
eFT4nubm6TPyBzcOS7Po8OSsnvFSL0vH95pcUZrx+Z9G8vStymTtOLGKLnd691Q89TRyUj1spV5T
Kweu0OmPhioTu5LilO10ep+xPS9L6/ue9OREwuy3k6/paKgyWYeXisHpPv+Spb/ooNX+uuKbW2+U
p5GD93QpQ2lg0JTzn86+aRqFJJ6Ju8HQTo9q7JvGt5YOn/5IRSlXfIMqzPNS99xTnpHKTxvTSDK3
mPnas5Nxy/evX0wk1Jca9XX3/DSq6GXf37/g6OY9dRXNTJrv/jInwsIbVZ2Kz1W0sr5lNcaFyF8t
LU72+pbmpqfxTwuWem66btamQ6c/GhMJWuoq+nAicREx49N/3pB5WWocCjET1jZ2VxLYnpel+wd9
KSoGX9eXeKmXpVZgH56cSYPbUWozSnUwvbyn0o1sYND2M8PQy1LHU2LPt+b7n4Ql3y1dvKfG9F6O
UupKGnu+nrmQrmpUfNsqzFS2NmZi3fKoDqZXdVQoP93unu+teNrA9JKpNY6Qsp5HxUVL3UFjXypV
8W0ztsWhpYynyU2ulKp9uN6PyXtK3//UUnzrSsr3w/P93LLUc9PgLDrlUeHaNLZyKc9NI9g91+Qs
nPVFKZN2FZ1Ey6WmkeFeX6TeKFfRedn35VHW5Pq7Qc+W2vcZ+VPzqPDGYS69pp6bxlaGkmPpnpb/
KS1lLpVkFqxrKEot+/5zlMpGbVq3LD3i68vwP60DS12bJiP/kdJVut4/QHZlZxpZqqNKW0fpzYMC
Wi4700giZWsO/VP7ftHSbUs5NxWqM83SXOj1ZaM2Pb6nqvjSzIJ/ckjPljKeHvZQbnnU8TbHAWYi
G9PIQ7TcPXBPj3Op+5m352Xp8enrnt7KpY5zqfO8b/qylLlUUeHymQvp7xO2lG+Ua9NP0fLN91Xx
lbGSNfVGuTYt8kqW5qbprO+PnfDZ0vl9PPVruulRlfSo4XtKHIpUQ9OdCcdT16bFjcMjtfCatnTK
912bFvPTo0y6HKVq6fRdm37qkLyjkHT6xkzUkUd5Gpmc8DLrC2tx5lKUuhq51GSU2sqlnPPfyqVc
mxbZb0ubsTkavk+Pcm1KnHRJmyPMieD8tFGbutcnRowwpjeAk45OxUdEd2o/KrxzlkDLRWMaGcyl
UnPTuBqdiVuWNjC95EPRRGKXO2jIpaJRRUcRfSzWpt3YOXvWOnpZGj/ooKnPHw2WkeI/ZZRKsjdU
qi9VfKMC86hoTCMzDyM/+1LhzsQlS68f/FNtcqV1JPKZtemKRhVdRHcw68vgG5Wpe+oq+iYS4VZf
6hOJ9/trSt93xXdzm+PWBvdxfnoPWsqs71bFNy52e6zNcQctddbHedQt3p6y74NR+GWp4yk3ZI72
TvI460vlUv9W0f8AAAD//6zd6aozNwwG4CtqkWyPl/u/sXJyKJR8YR6Vyf9AhLyMJb3LTU4b36fS
lZzGoK3QfeqKb1MJawslv7LQ6dUt5WnkUfc8Q/t0uzOxu159nkZ+6jr9z1tq2ZXppE5UoYr2PEo5
ne1xvX+Vqmh+9/v1vNfnE+WzH+5KKlJ7HMJFJEZhGjmJlHUVXfju33txxijUpoPfqE9dljddSb+l
7pWvfyK1ysgkO05s40+g3/c6amr1C7WpKz5qdRYwaPc6vT+Rjuerf2keNczhHYuRFljxRB+f59od
Y/JEtW/sU2ki+Ox39PpGSf3WvGgxuH2fDrCORgUpe4jqbENn36uPN/+oIGW/oNNrxIwY3KMwjexc
/aG+VDuFnOrmd23aOOXpi2qNBQ4vc2q+aTNDhro91kPZ+kYV+KZEzDQpYqRzKrbxqCgMdfOipSzo
iYT0+kZF/ZZs4y4t+WY18ca3VHxDu4M3f0Gpdejmd206WO+beWBsj/j7o1CbXsT0Fr5Rvvm73lJR
YB0JM2GN7nCkW2ff08hOReFx5WN23Ejm1IptdLW3Tu9Mn6j7s98rfFP2pbpwKM16KG0fRWq+6SHr
SCj57ldfBxKhV7iRfKEMOQhV+KbMqaeRg6z4S7o93bOTvhjp9VwNZ8jrqJ/C2U9FalY80XJjk29q
xiFYR72i09usJS/tjr2evqR7oeLLsHJLl3aHX31gxffCNNLaHU1I2SgoYoRW366hxzf/lFt4K6iM
6D61a+hpcgw+8jY+YTfGrn1acA1t9Dhc8opPe3FCq7NXXEMncyrP2L3tcQgtpF5xDWWvL1JOrJPe
xmfyRPEtFZydRFvPc4peX68oDHUqDHV5xh67MYZOlKvoZBWd6vakFduyK6cFhSHf/Js9FDsH3jsI
/URqje6lSPtW97zgIzG5+p5IhPVP5cpUUBVd+kYVquj2XBPBL5QRuk8LOr2cmo/9he45FDFaCdU5
nmMlPY+C0kCroDo7veM28ad2DjxLkVpnwt2eSU0E5vTCW6pVlFqpg3apiv5kLfiHyx1X31xz9qSn
8PyfhH3+QB9fitQ6E43Y86mzb7zUxNS8FeqomVQakP7pZdaREN2tgOqci2yOTvQxV39yn1a4kWRw
X2Rwj6fo41bhRhrXJ170snLLQg+lVVCdVBZc+xKLs4CX0tkvcCOJ6N4h/GkUlK95oozsoo/E7sJK
DiO7JiPlfbr5Nd2Xcmreydrap66jtpmx0pX8tIHeUZ2YnbRCHbXJi95ytl6zkFO9UFxHfdIQeV/9
pX1qpOyl+9R11OZ835Far2+HvlEFbiT1+jZZ8a5NC/vUOhPsnm9VfJ9gwe/7FGi5LEzONrs9R7o9
yxqIG33+NP40Qz3pjE7eibU7unJK/GnGQm2aqT7/4Zs/A32pdBWdIY/DTDldHX73M7hPWUUn5/s5
wDnLIPb8Nx/3keqWyqEXSl5gHWWnr3nCL/onUt1SOcU1z9XuVz8H9VB+83EfaTKn0pRNsTizE4GY
MxlpcJ/qJZ3CSafn+7/5uI2UVXTO1ImaUBTOPrxPMTtJV3w55SKSuz2P9MVYvo/00jxq60T1cduZ
iL/P2uFImdPBSO8Z3K/BGGZ8a3IaucHizMI0ct/f/D+/uJ3vv3LKCe8OfaM8jdxLE969NTdF//SV
D519VHyvv0muvvbp8erzGxXbq38Y6RdWP3WigkiEvZnTAyTCxte0ltMCEmFpvn9pFo16/5UPnai4
jEPRLXWEQ9lgyPz7L/eRsuILujCH9ulZzOlZevMbfxpEdIf6/GGNmZg3+3T8la2AP01iJlLM2Eqk
FyNlFR3En6b6/GHHi7zDTLwiNYszOTlLseLD2sd59zX9jXQaLacOWv5nn/4DAAD//6zdQa7DIAyE
4f07TY1JAofr3Z/UrKpGfJaSTdcjVDDBM/4vlYZd8h8DyFrpZh8KiWyv/b67YxtS2k1kG1xT8aPc
31/6UE6lfOsLOmVDCe4COy4alZpyxxxfiMwS7kgsOWcfpQXCJd/5m3LR4fM0VpNaT6UmXJLEar+U
Jwq3lVP2VHo8QBBq9EuZar/vUlpIcdqFpOSBX8+XBKFTqT1o7PHlGPeTXN5RTh3ZJa9+VPpNOg8q
jQcod9pRzRmJbKpRhRQnuebORroflV11333TxrqfG4nBTh5MralTnIUMr/Z+DJ+nXedpgXDJ/P4V
bPFb6SwQLqm032dxpiYLNk8ZaZP/U2d5Bu+nk0pdoxrX1Ocp7/wpJ8JV2Ocnccg1fSIbOcWOM0ci
Ufejwo3cuaOmfNJZSHKFlB4PpDiZNS+w4/aUUt+lOPu4a3JL9oKjm2u6PVD3NZ8/TRLQDSUK36Y+
pQp130q/eXx/5+/7HwAA//8DAAAft5FK9gQA
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 6ef64fc5bd56d90d-AMS
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 21 Mar 2022 11:22:47 GMT
Expect-CT:
- max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie
Via:
- 1.1 vegur
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.27.1
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&from_currency=CHF&function=CURRENCY_EXCHANGE_RATE&to_currency=USD
response:
body:
string: !!binary |
H4sIAAAAAAAAA3zQT0vDMBgG8Ps+xUPOLjRpt9netLN4EJG2u+wioX23FdsEkojK2HeXVtToxPP7
e95/xxkAsJJU77uBkD9bS7p5w81rc1B6TyiVJ5bhOMEJC47CmuHxy+amHQnLbwt28e3kb3evhslV
L51zKKzSTehjjtqcd91U61AlP9Vnz43uPLWovPLksDZ9r2wYW/Czk5jg0SoR8TKNQrnkuFPOo6Sd
JXegdqQyknIexXMpIEQm0yxZhJkVRz2+b2v0xzZ1HpYvOa67Fg+2a/4ZnHJcuac/1YROs9M7AAAA
//8DAJHb3+OvAQAA
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 6ef65a0cef9b0ba5-AMS
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 21 Mar 2022 11:29:48 GMT
Expect-CT:
- max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie
Via:
- 1.1 vegur
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.27.1
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&from_currency=EUR&function=CURRENCY_EXCHANGE_RATE&to_currency=USD
response:
body:
string: !!binary |
H4sIAAAAAAAAA3zQy07CQBQG4D1P8WfWMpk5RYXutODKGFPaDRsyaY/S2HaS6ZBoCO9uWgIOl7g+
339uuxEAiJRN7auGkWyd47b4weK72Jj2k5EazyLGboAD1hIvzjbrk01s2ROxyFNx9+fo0r2Z5uC2
zoYwksjsdbt8OQ/V5Fwdm+Vt5bnE0hvPHea2ro0LY/fy6hahpVaRIopUKB8kXk3nkfKH427DZU9J
EY1VNCYNrWOaxRMKM48SWf+3lW0P22RJWJ5KPFcl3l1V/DN4JvHUfd1UA9qP9r8AAAD//wMAvdGY
OKgBAAA=
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 6ef659f85b93fa44-AMS
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 21 Mar 2022 11:29:44 GMT
Expect-CT:
- max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie
Via:
- 1.1 vegur
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.27.1
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&from_currency=GBP&function=CURRENCY_EXCHANGE_RATE&to_currency=EUR
response:
body:
string: !!binary |
H4sIAAAAAAAAA3zQwUvDMBQG8Pv+io+cXUjSTdfebKm7iJTaXbxIaOMaXBNIU1DG/vfRgJo58fx+
7/t477gAAFIrefB6UCgm55RpP1F+tL00e4VaekUyHAMMmFM8ODu8ftvCdjMh27wiNz9O/HZPcggu
d9rrsUdlJ9Ph2St30GYfryYUjb0uKHd1rFaX6iu+nJyN2ZpeXUM45SnbMMZYLG8pHuXoUas3p8Ze
dTMVTIglS5aCg/NMpFlysXNH0cyfe7EmJO+aIh5vKHLdoXK6/ac4pbgf3/9UAZ0WpzMAAAD//wMA
g84xEqoBAAA=
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 6ef65a05cefafa18-AMS
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 21 Mar 2022 11:29:46 GMT
Expect-CT:
- max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie
Via:
- 1.1 vegur
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.27.1
method: GET
uri: https://www.alphavantage.co/query?apikey=MOCK_API_KEY&from_currency=USD&function=CURRENCY_EXCHANGE_RATE&to_currency=JPY
response:
body:
string: !!binary |
H4sIAAAAAAAAA3zQwU7DMAwG4Pue4lfOLGqyDWhvo4PDhNDUlcO4IKv1WEWboCRIoGnvjtoJCEzg
qz//lr0fAYAomNrQdIz81Tk21Tuu36odmSdGQYFFhv0AB6wkbpztHr9sbuueiPv1Qpx9O/3b3VF3
dKYJXGMdKLDHwrYtuXhwIlHa0/jlahOr6U/1Gb6kFzLsGRs2MZ/Jk5uEUqlUM5X0FdtziVvyAQVv
Hfsd1z3WidbjZDLWCkplOs2mOp65kCj7Dz5YczyyzOP2pcRVU2Plmurf1anE3D//4QZ2GB0+AAAA
//8DAPApKWm2AQAA
headers:
Allow:
- GET, HEAD, OPTIONS
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 6ef659fe1dfb970e-AMS
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 21 Mar 2022 11:29:45 GMT
Expect-CT:
- max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Cookie
Via:
- 1.1 vegur
X-Frame-Options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
# IMPORTATION STANDARD
# IMPORTATION THIRDPARTY
# IMPORTATION INTERNAL
from gamestonk_terminal.helper_classes import ModelsNamespace as _models
from gamestonk_terminal.forex.technical_analysis import ta_api
def test_models():
assert isinstance(ta_api.models, _models)

View File

@@ -0,0 +1,493 @@
# IMPORTATION STANDARD
import os
from datetime import datetime
# IMPORTATION THIRDPARTY
import pandas as pd
import pytest
# IMPORTATION INTERNAL
from gamestonk_terminal.forex.technical_analysis import ta_controller
# pylint: disable=E1101
# pylint: disable=W0603
# pylint: disable=E1111
EMPTY_DF = pd.DataFrame()
MOCK_STOCK_DF = pd.read_csv(
"tests/gamestonk_terminal/forex/technical_analysis/csv/test_ta_controller/data_df.csv",
index_col=0,
)
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": [("User-Agent", None)],
"filter_query_parameters": [
("period1", "MOCK_PERIOD_1"),
("period2", "MOCK_PERIOD_2"),
("date", "MOCK_DATE"),
],
}
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"queue, expected",
[
(["load", "help"], []),
(["quit", "help"], ["help"]),
],
)
def test_menu_with_queue(expected, mocker, queue):
path_controller = "gamestonk_terminal.forex.technical_analysis.ta_controller"
# MOCK SWITCH
mocker.patch(
target=f"{path_controller}.TechnicalAnalysisController.switch",
return_value=["quit"],
)
result_menu = ta_controller.TechnicalAnalysisController(
ticker="MOCK_TICKER",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=queue,
).menu()
assert result_menu == expected
@pytest.mark.vcr(record_mode="none")
def test_menu_without_queue_completion(mocker):
path_controller = "gamestonk_terminal.forex.technical_analysis.ta_controller"
# ENABLE AUTO-COMPLETION : HELPER_FUNCS.MENU
mocker.patch(
target="gamestonk_terminal.feature_flags.USE_PROMPT_TOOLKIT",
new=True,
)
mocker.patch(
target="gamestonk_terminal.parent_classes.session",
)
mocker.patch(
target="gamestonk_terminal.parent_classes.session.prompt",
return_value="quit",
)
# DISABLE AUTO-COMPLETION : CONTROLLER.COMPLETER
# DISABLE AUTO-COMPLETION
mocker.patch.object(
target=ta_controller.gtff,
attribute="USE_PROMPT_TOOLKIT",
new=True,
)
mocker.patch(
target=f"{path_controller}.session",
)
mocker.patch(
target=f"{path_controller}.session.prompt",
return_value="quit",
)
result_menu = ta_controller.TechnicalAnalysisController(
ticker="MOCK_TICKER",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
).menu()
assert result_menu == []
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"mock_input",
["help", "homee help", "home help", "mock"],
)
def test_menu_without_queue_sys_exit(mock_input, mocker):
path_controller = "gamestonk_terminal.forex.technical_analysis.ta_controller"
# DISABLE AUTO-COMPLETION
mocker.patch.object(
target=ta_controller.gtff,
attribute="USE_PROMPT_TOOLKIT",
new=False,
)
mocker.patch(
target=f"{path_controller}.session",
return_value=None,
)
# MOCK USER INPUT
mocker.patch("builtins.input", return_value=mock_input)
# MOCK SWITCH
class SystemExitSideEffect:
def __init__(self):
self.first_call = True
def __call__(self, *args, **kwargs):
if self.first_call:
self.first_call = False
raise SystemExit()
return ["quit"]
mock_switch = mocker.Mock(side_effect=SystemExitSideEffect())
mocker.patch(
target=f"{path_controller}.TechnicalAnalysisController.switch",
new=mock_switch,
)
result_menu = ta_controller.TechnicalAnalysisController(
ticker="MOCK_TICKER",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
).menu()
assert result_menu == []
@pytest.mark.vcr(record_mode="none")
@pytest.mark.record_stdout
@pytest.mark.parametrize(
"start",
[
datetime.strptime("2021-12-01", "%Y-%m-%d"),
],
)
def test_print_help(start):
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_TICKER",
start=start,
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
)
controller.print_help()
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"an_input, expected_queue",
[
("", []),
(
"/help",
[
"home",
"help",
],
),
("help/help", ["help", "help"]),
("q", ["quit"]),
("h", []),
(
"r",
[
"quit",
"quit",
"reset",
"forex",
"from MOCK_FROM",
"to MOCK_TO",
"load",
"ta",
],
),
],
)
def test_switch(an_input, expected_queue):
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_FROM/MOCK_TO",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
)
queue = controller.switch(an_input=an_input)
assert queue == expected_queue
@pytest.mark.vcr(record_mode="none")
def test_call_cls(mocker):
mocker.patch("os.system")
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_TICKER/MOCK_TICKER",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
)
controller.call_cls([])
assert controller.queue == []
os.system.assert_called_once_with("cls||clear")
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"func, queue, expected_queue",
[
(
"call_exit",
[],
["quit", "quit", "quit"],
),
("call_exit", ["help"], ["quit", "quit", "quit", "help"]),
("call_home", [], ["quit", "quit"]),
("call_help", [], []),
("call_quit", [], ["quit"]),
("call_quit", ["help"], ["quit", "help"]),
(
"call_reset",
[],
[
"quit",
"quit",
"reset",
"forex",
"from MOCK_FROM",
"to MOCK_TO",
"load",
"ta",
],
),
(
"call_reset",
["help"],
[
"quit",
"quit",
"reset",
"forex",
"from MOCK_FROM",
"to MOCK_TO",
"load",
"ta",
"help",
],
),
],
)
def test_call_func_expect_queue(expected_queue, func, queue):
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_FROM/MOCK_TO",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=queue,
)
result = getattr(controller, func)([])
assert result is None
assert controller.queue == expected_queue
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"tested_func, other_args, mocked_func, called_args, called_kwargs",
[
(
"call_ema",
[
"1,2",
"--offset=2",
"--export=csv",
],
"overlap_view.view_ma",
[],
dict(),
),
(
"call_sma",
[
"1,2",
"--offset=2",
"--export=csv",
],
"overlap_view.view_ma",
[],
dict(),
),
(
"call_zlma",
[
"1,2",
"--offset=2",
"--export=csv",
],
"overlap_view.view_ma",
[],
dict(),
),
(
"call_cci",
[
"--length=1",
"--scalar=2",
"--export=csv",
],
"momentum_view.display_cci",
[],
dict(),
),
(
"call_macd",
[
"--fast=1",
"--slow=2",
"--signal=3",
"--export=csv",
],
"momentum_view.display_macd",
[],
dict(),
),
(
"call_rsi",
[
"1",
"--scalar=2",
"--drift=3",
"--export=csv",
],
"momentum_view.display_rsi",
[],
dict(),
),
(
"call_stoch",
[
"--fastkperiod=1",
"--slowdperiod=2",
"--slowkperiod=3",
"--export=csv",
],
"momentum_view.display_stoch",
[],
dict(),
),
(
"call_fisher",
[
"1",
"--export=csv",
],
"momentum_view.display_fisher",
[],
dict(),
),
(
"call_cg",
[
"1",
"--export=csv",
],
"momentum_view.display_cg",
[],
dict(),
),
(
"call_adx",
[
"1",
"--scalar=2",
"--drift=3",
"--export=csv",
],
"trend_indicators_view.display_adx",
[],
dict(),
),
(
"call_aroon",
[
"1",
"--scalar=2",
"--export=csv",
],
"trend_indicators_view.display_aroon",
[],
dict(),
),
(
"call_bbands",
[
"1",
"--std=2",
"--mamode=MOCK_MAMODE",
"--export=csv",
],
"volatility_view.display_bbands",
[],
dict(),
),
(
"call_donchian",
[
"--length_upper=1",
"--length_lower=2",
"--export=csv",
],
"volatility_view.display_donchian",
[],
dict(),
),
(
"call_fib",
[
"1",
"--start=2021-12-01",
"--end=2021-12-02",
"--export=csv",
],
"custom_indicators_view.fibonacci_retracement",
[],
dict(),
),
],
)
def test_call_func(
tested_func, mocked_func, other_args, called_args, called_kwargs, mocker
):
path_controller = "gamestonk_terminal.forex.technical_analysis.ta_controller"
if mocked_func:
mock = mocker.Mock()
mocker.patch(
target=f"{path_controller}.{mocked_func}",
new=mock,
)
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_FROM/MOCK_TO",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
)
# controller.screen_tickers = ["PM"]
getattr(controller, tested_func)(other_args)
if called_args or called_kwargs:
mock.assert_called_once_with(*called_args, **called_kwargs)
else:
mock.assert_called_once()
else:
controller = ta_controller.TechnicalAnalysisController(
ticker="MOCK_FROM/MOCK_TO",
start=datetime.strptime("2021-12-01", "%Y-%m-%d"),
interval="MOCK_INTERVAL",
data=MOCK_STOCK_DF,
queue=None,
)
# controller.screen_tickers = ["PM"]
getattr(controller, tested_func)(other_args)

View File

@@ -0,0 +1,23 @@
Currency Pair Loaded: MOCK_TICKER (from 2021-12-01)
Overlap:
ema exponential moving average
sma simple moving average
zlma zero lag moving average
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
Custom:
fib fibonacci retracement

View File

@@ -0,0 +1,34 @@
# IMPORTATION STANDARD
# IMPORTATION THIRDPARTY
import pytest
# IMPORTATION INTERNAL
from gamestonk_terminal.forex import av_model
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_query_parameters": [
("apikey", "MOCK_API_KEY"),
]
}
@pytest.mark.vcr
@pytest.mark.parametrize(
"from_symbol, to_symbol",
[
("EUR", "USD"),
("USD", "JPY"),
("GBP", "EUR"),
("CHF", "USD"),
],
)
def test_get_historical(from_symbol, to_symbol, recorder):
result = av_model.get_historical(
to_symbol=to_symbol,
from_symbol=from_symbol,
)
recorder.capture(result)

View File

@@ -0,0 +1,30 @@
# IMPORTATION STANDARD
# IMPORTATION THIRDPARTY
import pytest
# IMPORTATION INTERNAL
from gamestonk_terminal.forex import av_view
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_query_parameters": [
("apikey", "MOCK_API_KEY"),
]
}
@pytest.mark.vcr
@pytest.mark.parametrize(
"from_symbol, to_symbol",
[
("EUR", "USD"),
("USD", "JPY"),
("GBP", "EUR"),
("CHF", "USD"),
],
)
def test_display_quote(from_symbol, to_symbol):
av_view.display_quote(from_symbol=from_symbol, to_symbol=to_symbol)

View File

@@ -0,0 +1,12 @@
# IMPORTATION STANDARD
# IMPORTATION THIRDPARTY
# IMPORTATION INTERNAL
from gamestonk_terminal.helper_classes import ModelsNamespace as _models
from gamestonk_terminal.forex import forex_api
def test_models():
assert isinstance(forex_api.oanda.models, _models)
assert isinstance(forex_api.ta.models, _models)

View File

@@ -0,0 +1,385 @@
# IMPORTATION STANDARD
import os
# IMPORTATION THIRDPARTY
import pandas as pd
import pytest
# IMPORTATION INTERNAL
from gamestonk_terminal.forex import forex_controller
from gamestonk_terminal.forex.technical_analysis.ta_controller import (
TechnicalAnalysisController,
)
# pylint: disable=E1101
# pylint: disable=W0603
# pylint: disable=E1111
NON_EMPTY_DF = pd.DataFrame.from_dict(
data={
pd.Timestamp("2020-11-30 00:00:00"): {
"Close": 75.75,
},
pd.Timestamp("2020-12-01 00:00:00"): {
"Close": 77.02999877929688,
},
},
orient="index",
)
EMPTY_DF = pd.DataFrame()
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": [("User-Agent", None)],
"filter_query_parameters": [
("period1", "MOCK_PERIOD_1"),
("period2", "MOCK_PERIOD_2"),
("date", "MOCK_DATE"),
],
}
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"queue, expected",
[
(["load", "help"], []),
(["quit", "help"], ["help"]),
],
)
def test_menu_with_queue(expected, mocker, queue):
path_controller = "gamestonk_terminal.forex.forex_controller"
# MOCK SWITCH
mocker.patch(
target=f"{path_controller}.ForexController.switch",
return_value=["quit"],
)
result_menu = forex_controller.ForexController(queue=queue).menu()
assert result_menu == expected
@pytest.mark.vcr(record_mode="none")
def test_menu_without_queue_completion(mocker):
path_controller = "gamestonk_terminal.forex.forex_controller"
# ENABLE AUTO-COMPLETION : HELPER_FUNCS.MENU
mocker.patch(
target="gamestonk_terminal.feature_flags.USE_PROMPT_TOOLKIT",
new=True,
)
mocker.patch(
target="gamestonk_terminal.parent_classes.session",
)
mocker.patch(
target="gamestonk_terminal.parent_classes.session.prompt",
return_value="quit",
)
# DISABLE AUTO-COMPLETION : CONTROLLER.COMPLETER
mocker.patch.object(
target=forex_controller.gtff,
attribute="USE_PROMPT_TOOLKIT",
new=True,
)
mocker.patch(
target=f"{path_controller}.session",
)
mocker.patch(
target=f"{path_controller}.session.prompt",
return_value="quit",
)
result_menu = forex_controller.ForexController(queue=None).menu()
assert result_menu == []
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"mock_input",
["help", "homee help", "home help", "mock"],
)
def test_menu_without_queue_sys_exit(mock_input, mocker):
path_controller = "gamestonk_terminal.forex.forex_controller"
# DISABLE AUTO-COMPLETION
mocker.patch.object(
target=forex_controller.gtff,
attribute="USE_PROMPT_TOOLKIT",
new=False,
)
mocker.patch(
target=f"{path_controller}.session",
return_value=None,
)
# MOCK USER INPUT
mocker.patch("builtins.input", return_value=mock_input)
# MOCK SWITCH
class SystemExitSideEffect:
def __init__(self):
self.first_call = True
def __call__(self, *args, **kwargs):
if self.first_call:
self.first_call = False
raise SystemExit()
return ["quit"]
mock_switch = mocker.Mock(side_effect=SystemExitSideEffect())
mocker.patch(
target=f"{path_controller}.ForexController.switch",
new=mock_switch,
)
result_menu = forex_controller.ForexController(queue=None).menu()
assert result_menu == []
@pytest.mark.vcr(record_mode="none")
@pytest.mark.record_stdout
def test_print_help():
controller = forex_controller.ForexController(queue=None)
controller.print_help()
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"an_input, expected_queue",
[
("", []),
("/help", ["home", "help"]),
("help/help", ["help", "help"]),
("q", ["quit"]),
("h", []),
(
"r",
[
"quit",
"reset",
"forex",
],
),
],
)
def test_switch(an_input, expected_queue):
controller = forex_controller.ForexController(queue=None)
queue = controller.switch(an_input=an_input)
assert queue == expected_queue
@pytest.mark.vcr(record_mode="none")
def test_call_cls(mocker):
mocker.patch("os.system")
controller = forex_controller.ForexController(queue=None)
controller.call_cls([])
assert controller.queue == []
os.system.assert_called_once_with("cls||clear")
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"func, queue, expected_queue",
[
(
"call_exit",
[],
["quit", "quit"],
),
("call_exit", ["help"], ["quit", "quit", "help"]),
("call_home", [], ["quit"]),
("call_help", [], []),
("call_quit", [], ["quit"]),
("call_quit", ["help"], ["quit", "help"]),
(
"call_reset",
[],
[
"quit",
"reset",
"forex",
],
),
(
"call_reset",
["help"],
[
"quit",
"reset",
"forex",
"help",
],
),
],
)
def test_call_func_expect_queue(expected_queue, func, queue):
controller = forex_controller.ForexController(queue=queue)
result = getattr(controller, func)([])
assert result is None
assert controller.queue == expected_queue
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"tested_func, other_args, mocked_func, called_args, called_kwargs",
[
("call_to", ["GBP"], None, [], dict()),
("call_from", ["EUR"], None, [], dict()),
(
"call_ta",
None,
None,
None,
None,
),
(
"call_ta",
[],
"ForexController.load_class",
[TechnicalAnalysisController],
dict(
ticker="MOCK_TICKER/MOCK_TICKER",
data=NON_EMPTY_DF,
start=pd.Timestamp("2020-11-30 00:00:00"),
interval="",
queue=[],
),
),
(
"call_oanda",
None,
"ForexController.load_class",
None,
None,
),
],
)
def test_call_func(
tested_func, mocked_func, other_args, called_args, called_kwargs, mocker
):
path_controller = "gamestonk_terminal.forex.forex_controller"
if mocked_func:
mock = mocker.Mock()
mocker.patch(
target=f"{path_controller}.{mocked_func}",
new=mock,
)
controller = forex_controller.ForexController(queue=None)
controller.data = NON_EMPTY_DF
controller.to_symbol = "MOCK_TICKER"
controller.from_symbol = "MOCK_TICKER"
controller.interval = ""
getattr(controller, tested_func)(other_args)
if called_args or called_kwargs:
mock.assert_called_once_with(*called_args, **called_kwargs)
else:
mock.assert_called_once()
else:
controller = forex_controller.ForexController(queue=None)
controller.stock = EMPTY_DF
controller.to_symbol = "MOCK_TICKER"
controller.from_symbol = "MOCK_TICKER"
controller.interval = "1440min"
getattr(controller, tested_func)(other_args)
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"func",
[
"call_from",
"call_to",
"call_load",
"call_candle",
],
)
def test_call_func_no_parser(func, mocker):
# MOCK PARSE_KNOWN_ARGS_AND_WARN
mocker.patch(
target="gamestonk_terminal.forex.forex_controller.parse_known_args_and_warn",
return_value=None,
)
controller = forex_controller.ForexController(queue=None)
func_result = getattr(controller, func)(other_args=list())
assert func_result is None
assert controller.queue == []
getattr(forex_controller, "parse_known_args_and_warn").assert_called_once()
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"func",
[
"call_candle",
"call_ta",
# "call_qa",
# "call_pred",
],
)
def test_call_func_no_ticker(func, mocker):
# MOCK PARSE_KNOWN_ARGS_AND_WARN
mocker.patch(
"gamestonk_terminal.forex.forex_controller.parse_known_args_and_warn",
return_value=True,
)
controller = forex_controller.ForexController(queue=None)
func_result = getattr(controller, func)([])
assert func_result is None
assert controller.queue == []
@pytest.mark.vcr(record_mode="none")
@pytest.mark.parametrize(
"from_symbol, to_symbol, expected",
[
(None, None, []),
("MOCK_FROM", None, []),
(None, "MOCK_TO", []),
("MOCK_FROM", "MOCK_TO", ["forex", "from MOCK_FROM", "to MOCK_TO"]),
],
)
def test_custom_reset(from_symbol, to_symbol, expected):
controller = forex_controller.ForexController(queue=None)
controller.from_symbol = from_symbol
controller.to_symbol = to_symbol
result = controller.custom_reset()
assert result == expected
# @pytest.mark.vcr
# def test_call_load(mocker):
# # FORCE SINGLE THREADING
# yf_download = forex_controller.forex_helper.yf.download
# def mock_yf_download(*args, **kwargs):
# kwargs["threads"] = False
# return yf_download(*args, **kwargs)
# mocker.patch("yfinance.download", side_effect=mock_yf_download)
# controller = forex_controller.ForexController(queue=None)
# other_args = [
# "TSLA",
# "--start=2021-12-17",
# "--end=2021-12-18",
# ]
# controller.call_load(other_args=other_args)

View File

@@ -0,0 +1,18 @@
from select the "from" currency in a forex pair
to select the "to" currency in a forex pair
From: USD
To:
[dim]
[AlphaVantage]
quote get last quote
load get historical data
candle show candle plot for loaded data
> ta technical analysis for loaded coin, e.g.: ema, macd, rsi, adx, bbands, obv
[/dim]
Forex brokerages:
> oanda Oanda menu