From a12e0946da4d85baa6590eb2a06cd440b42734a5 Mon Sep 17 00:00:00 2001 From: jose-donato <43375532+jose-donato@users.noreply.github.com> Date: Thu, 14 Apr 2022 08:24:42 +0100 Subject: [PATCH] Starts crypto tools menu (#1668) * started crypto tools menu * allow zero float * added obb script Co-authored-by: Chavithra Co-authored-by: didierlopes.eth Co-authored-by: minhhoang1023 <40023817+minhhoang1023@users.noreply.github.com> --- .../cryptocurrency/crypto_controller.py | 23 +- .../cryptocurrency/tools/__init__.py | 0 .../cryptocurrency/tools/tools_api.py | 13 + .../cryptocurrency/tools/tools_controller.py | 180 ++++++++++++++ .../cryptocurrency/tools/tools_helpers.py | 58 +++++ .../cryptocurrency/tools/tools_model.py | 118 +++++++++ .../cryptocurrency/tools/tools_view.py | 104 ++++++++ openbb_terminal/helper_funcs.py | 25 ++ scripts/test_crypto_tools.openbb | 5 + .../cryptocurrency/test_crypto_controller.py | 7 + .../cryptocurrency/tools/__init__.py | 0 .../test_calculate_apy[100-12].csv | 4 + .../test_calculate_il[100-200-50-1000].csv | 8 + .../tools/test_tools_controller.py | 224 ++++++++++++++++++ .../cryptocurrency/tools/test_tools_model.py | 33 +++ .../cryptocurrency/tools/test_tools_view.py | 37 +++ .../test_tools_controller/test_print_help.txt | 4 + .../txt/test_tools_view/test_display_apy.txt | 5 + .../txt/test_tools_view/test_display_il.txt | 9 + .../test_print_help.txt | 1 + .../terminal/cryptocurrency/tools/_index.md | 5 + .../cryptocurrency/tools/aprtoapy/_index.md | 13 + .../cryptocurrency/tools/il/_index.md | 20 ++ website/data/menu/main.yml | 7 + 24 files changed, 902 insertions(+), 1 deletion(-) create mode 100644 openbb_terminal/cryptocurrency/tools/__init__.py create mode 100644 openbb_terminal/cryptocurrency/tools/tools_api.py create mode 100644 openbb_terminal/cryptocurrency/tools/tools_controller.py create mode 100644 openbb_terminal/cryptocurrency/tools/tools_helpers.py create mode 100644 openbb_terminal/cryptocurrency/tools/tools_model.py create mode 100644 openbb_terminal/cryptocurrency/tools/tools_view.py create mode 100644 scripts/test_crypto_tools.openbb create mode 100644 tests/openbb_terminal/cryptocurrency/tools/__init__.py create mode 100644 tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_apy[100-12].csv create mode 100644 tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_il[100-200-50-1000].csv create mode 100644 tests/openbb_terminal/cryptocurrency/tools/test_tools_controller.py create mode 100644 tests/openbb_terminal/cryptocurrency/tools/test_tools_model.py create mode 100644 tests/openbb_terminal/cryptocurrency/tools/test_tools_view.py create mode 100644 tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_controller/test_print_help.txt create mode 100644 tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_apy.txt create mode 100644 tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_il.txt create mode 100644 website/content/terminal/cryptocurrency/tools/_index.md create mode 100644 website/content/terminal/cryptocurrency/tools/aprtoapy/_index.md create mode 100644 website/content/terminal/cryptocurrency/tools/il/_index.md diff --git a/openbb_terminal/cryptocurrency/crypto_controller.py b/openbb_terminal/cryptocurrency/crypto_controller.py index 8ec1844df0a..3eb3472f7ba 100644 --- a/openbb_terminal/cryptocurrency/crypto_controller.py +++ b/openbb_terminal/cryptocurrency/crypto_controller.py @@ -63,7 +63,18 @@ class CryptoController(CryptoBaseController): "prt", "resources", ] - CHOICES_MENUS = ["ta", "dd", "ov", "disc", "onchain", "defi", "nft", "pred", "qa"] + CHOICES_MENUS = [ + "ta", + "dd", + "ov", + "disc", + "onchain", + "defi", + "tools", + "nft", + "pred", + "qa", + ] DD_VIEWS_MAPPING = { "cg": pycoingecko_view, @@ -106,6 +117,7 @@ class CryptoController(CryptoBaseController): > ov overview of the cryptocurrencies, e.g.: market cap, DeFi, latest news, top exchanges, stables > onchain information on different blockchains, e.g.: eth gas fees, whale alerts, DEXes info > defi decentralized finance information, e.g.: dpi, llama, tvl, lending, borrow, funding +> tools explore different tools e.g.: apytoapr, il > nft non-fungible tokens, e.g.: today drops{has_ticker_start} > dd due-diligence for loaded coin, e.g.: coin information, social media, market stats > ta technical analysis for loaded coin, e.g.: ema, macd, rsi, adx, bbands, obv @@ -430,6 +442,15 @@ class CryptoController(CryptoBaseController): else: console.print("No coin selected. Use 'load' to load a coin.\n") + @log_start_end(log=logger) + def call_tools(self, _): + """Process tools command""" + from openbb_terminal.cryptocurrency.tools.tools_controller import ( + ToolsController, + ) + + self.queue = self.load_class(ToolsController, self.queue) + @log_start_end(log=logger) def call_disc(self, _): """Process disc command""" diff --git a/openbb_terminal/cryptocurrency/tools/__init__.py b/openbb_terminal/cryptocurrency/tools/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openbb_terminal/cryptocurrency/tools/tools_api.py b/openbb_terminal/cryptocurrency/tools/tools_api.py new file mode 100644 index 00000000000..a3bca948e86 --- /dev/null +++ b/openbb_terminal/cryptocurrency/tools/tools_api.py @@ -0,0 +1,13 @@ +"""Tools context API.""" +import os +from openbb_terminal.helper_classes import ModelsNamespace as _models + +# flake8: noqa +# pylint: disable=unused-import + +# Menu commands +from .tools_view import display_apy as aprtoapy +from .tools_view import display_il as il + +# Models +models = _models(os.path.abspath(os.path.dirname(__file__))) diff --git a/openbb_terminal/cryptocurrency/tools/tools_controller.py b/openbb_terminal/cryptocurrency/tools/tools_controller.py new file mode 100644 index 00000000000..b2b74e7dc81 --- /dev/null +++ b/openbb_terminal/cryptocurrency/tools/tools_controller.py @@ -0,0 +1,180 @@ +"""Tools Controller Module""" +__docformat__ = "numpy" + +# pylint: disable=C0302 + +import argparse +import logging +from typing import List + +from prompt_toolkit.completion import NestedCompleter + +from openbb_terminal import feature_flags as obbff +from openbb_terminal.decorators import log_start_end +from openbb_terminal.helper_funcs import ( + EXPORT_ONLY_RAW_DATA_ALLOWED, + check_non_negative_float, + check_percentage_range, + check_positive, + check_positive_float, + parse_known_args_and_warn, +) +from openbb_terminal.cryptocurrency.tools import tools_view +from openbb_terminal.menu import session +from openbb_terminal.parent_classes import BaseController +from openbb_terminal.rich_config import console + +logger = logging.getLogger(__name__) + + +class ToolsController(BaseController): + """Tools Controller class""" + + CHOICES_COMMANDS = ["aprtoapy", "il"] + + PATH = "/crypto/tools/" + + def __init__(self, queue: List[str] = None): + """Constructor""" + super().__init__(queue) + + if session and obbff.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""" + + help_text = """[cmds] + aprtoapy convert apr to apy + il calculate impermanent loss[/cmds] +""" + console.print(text=help_text, menu="Cryptocurrency - Tools") + + @log_start_end(log=logger) + def call_il(self, other_args: List[str]): + """Process il command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="il", + description="""Tool to calculate Impermanent Loss in a custom liquidity pool. + Users can provide percentages increases for two tokens (and their weight in the liquidity pool) + and verify the impermanent loss that can occur.""", + ) + + parser.add_argument( + "-pcA", + "--priceChangeA", + dest="priceChangeA", + type=check_non_negative_float, + help="Token A price change in percentage", + default=0, + ) + + parser.add_argument( + "-pcB", + "--priceChangeB", + dest="priceChangeB", + type=check_non_negative_float, + help="Token B price change in percentage", + default=100, + ) + parser.add_argument( + "-p", + "--proportion", + dest="proportion", + type=check_percentage_range, + help="""Pool proportion. E.g., 50 means that pool contains 50%% of token A and 50%% of token B, + 30 means that pool contains 30%% of token A and 70%% of token B""", + default=50, + ) + + parser.add_argument( + "-v", + "--value", + dest="value", + type=check_positive_float, + help="Initial amount of dollars that user provides to liquidity pool", + default=1000, + ) + + parser.add_argument( + "-n", + "--narrative", + dest="narrative", + action="store_true", + help="Flag to show narrative instead of dataframe", + default=False, + ) + + if other_args and not other_args[0][0] == "-": + other_args.insert(0, "-pcA") + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if ns_parser: + tools_view.display_il( + price_changeA=ns_parser.priceChangeA, + price_changeB=ns_parser.priceChangeB, + proportion=ns_parser.proportion, + initial_pool_value=ns_parser.value, + narrative=ns_parser.narrative, + export=ns_parser.export, + ) + + @log_start_end(log=logger) + def call_aprtoapy(self, other_args: List[str]): + """Process aprtoapy command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="aprtoapy", + description=""" + Tool to calculate APY from APR value. + Compouding periods, i.e., the number of times compounded per year + can be defined with -c argument. + """, + ) + + parser.add_argument( + "--apr", + dest="apr", + type=check_positive_float, + help="APR value in percentage to convert", + default=100, + ) + + parser.add_argument( + "-c", + "--compounding", + dest="compounding", + type=check_positive, + help="Number of compounded periods in a year. 12 means compounding monthly", + default=12, + ) + parser.add_argument( + "-n", + "--narrative", + dest="narrative", + action="store_true", + help="Flag to show narrative instead of dataframe", + default=False, + ) + + if other_args and not other_args[0][0] == "-": + other_args.insert(0, "--apr") + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED + ) + + if ns_parser: + tools_view.display_apy( + apr=ns_parser.apr, + compounding_times=ns_parser.compounding, + narrative=ns_parser.narrative, + export=ns_parser.export, + ) diff --git a/openbb_terminal/cryptocurrency/tools/tools_helpers.py b/openbb_terminal/cryptocurrency/tools/tools_helpers.py new file mode 100644 index 00000000000..a471b756452 --- /dev/null +++ b/openbb_terminal/cryptocurrency/tools/tools_helpers.py @@ -0,0 +1,58 @@ +def change_variation(change: float) -> float: + """Helper to convert change variation + + Parameters + ---------- + change: float + percentage change + + Returns + ------- + float: + converted value + """ + return (100 + change) / 100 + + +def calculate_hold_value(changeA: float, changeB: float, proportion: float) -> float: + """Calculates hold value of two different coins + + Parameters + ---------- + changeA: float + price change of crypto A in percentage + changeB: float + price change of crypto B in percentage + proportion: float + percentage of first token in pool + + Returns + ------- + float: + hold value + """ + return (change_variation(changeA) * proportion) / 100 + ( + (change_variation(changeB) * (100 - proportion)) / 100 + ) + + +def calculate_pool_value(changeA, changeB, proportion): + """Calculates pool value of two different coins + + Parameters + ---------- + changeA: float + price change of crypto A in percentage + changeB: float + price change of crypto B in percentage + proportion: float + percentage of first token in pool + + Returns + ------- + float: + pool value + """ + return pow(change_variation(changeA), proportion / 100) * pow( + change_variation(changeB), (100 - proportion) / 100 + ) diff --git a/openbb_terminal/cryptocurrency/tools/tools_model.py b/openbb_terminal/cryptocurrency/tools/tools_model.py new file mode 100644 index 00000000000..b14784024a0 --- /dev/null +++ b/openbb_terminal/cryptocurrency/tools/tools_model.py @@ -0,0 +1,118 @@ +"""Tools Model""" +__docformat__ = "numpy" + +import logging +from typing import Tuple + +import pandas as pd + +from openbb_terminal.cryptocurrency.tools.tools_helpers import ( + calculate_hold_value, + calculate_pool_value, +) +from openbb_terminal.decorators import log_start_end + +logger = logging.getLogger(__name__) + + +@log_start_end(log=logger) +def calculate_apy(apr: float, compounding_times: int) -> Tuple[pd.DataFrame, str]: + """Converts apr into apy + + Parameters + ---------- + apr: float + value in percentage + compounding_times: int + number of compounded periods in a year + + Returns + ------- + Tuple: + - pd.DataFrame: dataframe with results + - str: narrative version of results + """ + apy = (pow((1 + (apr / 100) / compounding_times), compounding_times) - 1) * 100 + apy_str = f""" +An APR of {apr}% compounded {compounding_times} times per year equals to an APY of {round(apy,3)}% + """ + df = pd.DataFrame( + { + "Metric": [ + "APR", + "Compounding Times", + "APY", + ], + "Value": [ + f"{apr}%", + compounding_times, + f"{round(apy, 3)}%", + ], + } + ) + return df, apy_str + + +@log_start_end(log=logger) +def calculate_il( + price_changeA: float, + price_changeB: float, + proportion: float, + initial_pool_value: float, +) -> Tuple[pd.DataFrame, str]: + """Calculates Impermanent Loss in a custom liquidity pool + + Parameters + ---------- + price_changeA: float + price change of crypto A in percentage + price_changeB: float + price change of crypto B in percentage + proportion: float + percentage of first token in pool + initial_pool_value: float + initial value that pool contains + + Returns + ------- + Tuple: + - pd.DataFrame: dataframe with results + - str: narrative version of results + """ + pool_value = calculate_pool_value(price_changeA, price_changeB, proportion) + hold_value = calculate_hold_value(price_changeA, price_changeB, proportion) + il = abs(((pool_value / hold_value) - 1) * 100) + hold_value = hold_value * initial_pool_value + pool_value = pool_value * initial_pool_value + il_str = f""" +Ignoring fees/rewards and only accounting for impermanent loss: + +A change of {price_changeA}% in token A and {price_changeB}% in token B in +a pool with a proportion of {proportion}/{100-proportion} and with an initial +value of ${initial_pool_value} would result in an impermant loss of {round(il,2)}% +If you just hold the tokens you would have ${round(hold_value,2)} whereas +in the pool you would have ${round(pool_value,2)} + """ + df = pd.DataFrame( + { + "Metric": [ + "Price Change Token A", + "Price Change Token B", + "Initial Pool Value", + "Proportion", + "Impermanent Loss", + "Hold Value", + "Pool Value", + ], + "Value": [ + f"{price_changeA}%", + f"{price_changeB}%", + f"${initial_pool_value}", + f"{proportion}/{100-proportion}", + f"{round(il,2)}%", + f"${round(hold_value,2)}", + f"${round(pool_value,2)}", + ], + } + ) + return df, il_str diff --git a/openbb_terminal/cryptocurrency/tools/tools_view.py b/openbb_terminal/cryptocurrency/tools/tools_view.py new file mode 100644 index 00000000000..93545da27eb --- /dev/null +++ b/openbb_terminal/cryptocurrency/tools/tools_view.py @@ -0,0 +1,104 @@ +"""Tools View""" +__docformat__ = "numpy" + +import logging +import os + +from openbb_terminal.cryptocurrency.tools.tools_model import calculate_apy, calculate_il +from openbb_terminal.helper_funcs import export_data, print_rich_table +from openbb_terminal.rich_config import console +from openbb_terminal.decorators import log_start_end + +logger = logging.getLogger(__name__) + + +@log_start_end(log=logger) +def display_apy(apr: float, compounding_times: int, narrative: bool, export: str): + """Displays APY value converted from APR + + Parameters + ---------- + apr: float + value in percentage + compounding_times: int + number of compounded periods in a year + narrative: str + display narrative version instead of dataframe + export : str + Export dataframe data to csv,json,xlsx file + + Returns + ------- + """ + df, apy_str = calculate_apy(apr, compounding_times) + + if narrative: + console.print(apy_str) + else: + print_rich_table( + df, + headers=list(df.columns), + show_index=False, + title="APR/APY Calculator", + ) + console.print("") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "aprtoapy", + apy_str, + ) + + +@log_start_end(log=logger) +def display_il( + price_changeA: int, + price_changeB: int, + proportion: int, + initial_pool_value: int, + narrative: bool, + export: str, +): + """Displays Impermanent Loss in a custom liquidity pool + + Parameters + ---------- + price_changeA: float + price change of crypto A in percentage + price_changeB: float + price change of crypto B in percentage + proportion: float + percentage of first token in pool + initial_pool_value: float + initial value that pool contains + narrative: str + display narrative version instead of dataframe + export : str + Export dataframe data to csv,json,xlsx file + + Returns + ------- + """ + df, il_str = calculate_il( + price_changeA=price_changeA, + price_changeB=price_changeB, + proportion=proportion, + initial_pool_value=initial_pool_value, + ) + if narrative: + console.print(il_str) + else: + print_rich_table( + df, + headers=list(df.columns), + show_index=False, + title="Impermanent Loss Calculator", + ) + console.print("") + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "il", + df, + ) diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index db8d6e1a080..5b200003dec 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -317,6 +317,31 @@ def check_positive_float(value) -> float: return new_value +def check_percentage_range(num) -> float: + """ + Checks if float is between 0 and 100. If so, return it. + + Parameters + ---------- + num: float + Input float + Returns + ------- + num: float + Input number if conditions are met + Raises + ------- + argparse.ArgumentTypeError + Input number not between min and max values + """ + num = float(num) + maxi = 100.0 + mini = 0.0 + if num <= mini or num >= maxi: + log_and_raise(argparse.ArgumentTypeError("Value must be between 0 and 100")) + return num + + def check_proportion_range(num) -> float: """ Checks if float is between 0 and 1. If so, return it. diff --git a/scripts/test_crypto_tools.openbb b/scripts/test_crypto_tools.openbb new file mode 100644 index 00000000000..ea3d546aad6 --- /dev/null +++ b/scripts/test_crypto_tools.openbb @@ -0,0 +1,5 @@ +crypto +tools +aprtoapy +il +exit \ No newline at end of file diff --git a/tests/openbb_terminal/cryptocurrency/test_crypto_controller.py b/tests/openbb_terminal/cryptocurrency/test_crypto_controller.py index 5e0b9391349..1f5c9c9259f 100644 --- a/tests/openbb_terminal/cryptocurrency/test_crypto_controller.py +++ b/tests/openbb_terminal/cryptocurrency/test_crypto_controller.py @@ -292,6 +292,13 @@ def test_call_func_expect_queue(expected_queue, func, queue): [], dict(), ), + ( + "call_tools", + [], + "CryptoController.load_class", + [], + dict(), + ), ], ) def test_call_func( diff --git a/tests/openbb_terminal/cryptocurrency/tools/__init__.py b/tests/openbb_terminal/cryptocurrency/tools/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_apy[100-12].csv b/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_apy[100-12].csv new file mode 100644 index 00000000000..7ae06c1e8ab --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_apy[100-12].csv @@ -0,0 +1,4 @@ +,Metric,Value +0,APR,100% +1,Compounding Times,12 +2,APY,161.304% diff --git a/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_il[100-200-50-1000].csv b/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_il[100-200-50-1000].csv new file mode 100644 index 00000000000..477d591e047 --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/csv/test_tools_model/test_calculate_il[100-200-50-1000].csv @@ -0,0 +1,8 @@ +,Metric,Value +0,Price Change Token A,100% +1,Price Change Token B,200% +2,Initial Pool Value,$1000 +3,Proportion,50/50 +4,Impermanent Loss,2.02% +5,Hold Value,$2500.0 +6,Pool Value,$2449.49 diff --git a/tests/openbb_terminal/cryptocurrency/tools/test_tools_controller.py b/tests/openbb_terminal/cryptocurrency/tools/test_tools_controller.py new file mode 100644 index 00000000000..5d9b90d234e --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/test_tools_controller.py @@ -0,0 +1,224 @@ +# IMPORTATION STANDARD +import os + +# IMPORTATION THIRDPARTY +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.cryptocurrency.tools import tools_controller + +# pylint: disable=E1101 +# pylint: disable=W0603 +# pylint: disable=E1111 + + +@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 = "openbb_terminal.cryptocurrency.tools.tools_controller" + + # MOCK SWITCH + mocker.patch( + target=f"{path_controller}.ToolsController.switch", + return_value=["quit"], + ) + result_menu = tools_controller.ToolsController(queue=queue).menu() + + assert result_menu == expected + + +@pytest.mark.vcr(record_mode="none") +def test_menu_without_queue_completion(mocker): + path_controller = "openbb_terminal.cryptocurrency.tools.tools_controller" + + # ENABLE AUTO-COMPLETION : HELPER_FUNCS.MENU + mocker.patch( + target="openbb_terminal.feature_flags.USE_PROMPT_TOOLKIT", + new=True, + ) + mocker.patch( + target="openbb_terminal.parent_classes.session", + ) + mocker.patch( + target="openbb_terminal.parent_classes.session.prompt", + return_value="quit", + ) + + # DISABLE AUTO-COMPLETION : CONTROLLER.COMPLETER + mocker.patch.object( + target=tools_controller.obbff, + 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 = tools_controller.ToolsController(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 = "openbb_terminal.cryptocurrency.tools.tools_controller" + + # DISABLE AUTO-COMPLETION + mocker.patch.object( + target=tools_controller.obbff, + 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}.ToolsController.switch", + new=mock_switch, + ) + + result_menu = tools_controller.ToolsController(queue=None).menu() + + assert result_menu == [] + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +def test_print_help(): + controller = tools_controller.ToolsController(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", "crypto", "tools"], + ), + ], +) +def test_switch(an_input, expected_queue): + controller = tools_controller.ToolsController(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 = tools_controller.ToolsController(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", "crypto", "tools"], + ), + ( + "call_reset", + ["help"], + ["quit", "quit", "reset", "crypto", "tools", "help"], + ), + ], +) +def test_call_func_expect_queue(expected_queue, func, queue): + controller = tools_controller.ToolsController(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_aprtoapy", + [], + "tools_view.display_apy", + [], + dict(), + ), + ], +) +def test_call_func( + tested_func, mocked_func, other_args, called_args, called_kwargs, mocker +): + path_controller = "openbb_terminal.cryptocurrency.tools.tools_controller" + + if mocked_func: + mock = mocker.Mock() + mocker.patch( + target=f"{path_controller}.{mocked_func}", + new=mock, + ) + + controller = tools_controller.ToolsController(queue=None) + 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 = tools_controller.ToolsController(queue=None) + getattr(controller, tested_func)(other_args) diff --git a/tests/openbb_terminal/cryptocurrency/tools/test_tools_model.py b/tests/openbb_terminal/cryptocurrency/tools/test_tools_model.py new file mode 100644 index 00000000000..2cb5bf489b8 --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/test_tools_model.py @@ -0,0 +1,33 @@ +# IMPORTATION STANDARD + +# IMPORTATION THIRDPARTY +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.cryptocurrency.tools import tools_model + + +@pytest.mark.vcr +@pytest.mark.parametrize( + "apr,compound_times", + [(100, 12)], +) +def test_calculate_apy(apr, compound_times, recorder): + df, _ = tools_model.calculate_apy(apr=apr, compounding_times=compound_times) + recorder.capture(df) + + +@pytest.mark.vcr +@pytest.mark.parametrize( + "price_changeA,price_changeB,proportion,initial_pool_value", [(100, 200, 50, 1000)] +) +def test_calculate_il( + price_changeA, price_changeB, proportion, initial_pool_value, recorder +): + df, _ = tools_model.calculate_il( + price_changeA=price_changeA, + price_changeB=price_changeB, + proportion=proportion, + initial_pool_value=initial_pool_value, + ) + recorder.capture(df) diff --git a/tests/openbb_terminal/cryptocurrency/tools/test_tools_view.py b/tests/openbb_terminal/cryptocurrency/tools/test_tools_view.py new file mode 100644 index 00000000000..535422e2bb3 --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/test_tools_view.py @@ -0,0 +1,37 @@ +# IMPORTATION STANDARD + +# IMPORTATION THIRDPARTY +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.cryptocurrency.tools import tools_view + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_il(mocker): + # MOCK EXPORT_DATA + mocker.patch(target="openbb_terminal.cryptocurrency.tools.tools_view.export_data") + + tools_view.display_il( + price_changeA=100, + price_changeB=200, + proportion=50, + initial_pool_value=1000, + narrative=False, + export="", + ) + + +@pytest.mark.vcr +@pytest.mark.record_stdout +def test_display_apy(mocker): + # MOCK EXPORT_DATA + mocker.patch(target="openbb_terminal.cryptocurrency.tools.tools_view.export_data") + + tools_view.display_apy( + apr=100, + compounding_times=12, + narrative=False, + export="", + ) diff --git a/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_controller/test_print_help.txt b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_controller/test_print_help.txt new file mode 100644 index 00000000000..5a7ed01136f --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_controller/test_print_help.txt @@ -0,0 +1,4 @@ + + aprtoapy convert apr to apy + il calculate impermanent loss + diff --git a/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_apy.txt b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_apy.txt new file mode 100644 index 00000000000..41df5bfeefa --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_apy.txt @@ -0,0 +1,5 @@ + Metric Value +0 APR 100% +1 Compounding Times 12 +2 APY 161.304% + diff --git a/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_il.txt b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_il.txt new file mode 100644 index 00000000000..1e41beb2ee1 --- /dev/null +++ b/tests/openbb_terminal/cryptocurrency/tools/txt/test_tools_view/test_display_il.txt @@ -0,0 +1,9 @@ + Metric Value +0 Price Change Token A 100% +1 Price Change Token B 200% +2 Initial Pool Value $1000 +3 Proportion 50/50 +4 Impermanent Loss 2.02% +5 Hold Value $2500.0 +6 Pool Value $2449.49 + diff --git a/tests/openbb_terminal/cryptocurrency/txt/test_crypto_controller/test_print_help.txt b/tests/openbb_terminal/cryptocurrency/txt/test_crypto_controller/test_print_help.txt index b88f693bdd9..15098310f2d 100644 --- a/tests/openbb_terminal/cryptocurrency/txt/test_crypto_controller/test_print_help.txt +++ b/tests/openbb_terminal/cryptocurrency/txt/test_crypto_controller/test_print_help.txt @@ -13,6 +13,7 @@ Source: > ov overview of the cryptocurrencies, e.g.: market cap, DeFi, latest news, top exchanges, stables > onchain information on different blockchains, e.g.: eth gas fees, whale alerts, DEXes info > defi decentralized finance information, e.g.: dpi, llama, tvl, lending, borrow, funding +> tools explore different tools e.g.: apytoapr, il > nft non-fungible tokens, e.g.: today drops[unvl] > dd due-diligence for loaded coin, e.g.: coin information, social media, market stats > ta technical analysis for loaded coin, e.g.: ema, macd, rsi, adx, bbands, obv diff --git a/website/content/terminal/cryptocurrency/tools/_index.md b/website/content/terminal/cryptocurrency/tools/_index.md new file mode 100644 index 00000000000..093e02e215d --- /dev/null +++ b/website/content/terminal/cryptocurrency/tools/_index.md @@ -0,0 +1,5 @@ +--- +geekdocCollapseSection: true +--- + +{{< toc-tree >}} diff --git a/website/content/terminal/cryptocurrency/tools/aprtoapy/_index.md b/website/content/terminal/cryptocurrency/tools/aprtoapy/_index.md new file mode 100644 index 00000000000..4adac23f462 --- /dev/null +++ b/website/content/terminal/cryptocurrency/tools/aprtoapy/_index.md @@ -0,0 +1,13 @@ +`usage: aprtoapy [--apr APR] [-c COMPOUNDING] [-n] [-h] [--export EXPORT]` + +Tool to calculate APY from APR value. Compouding periods, i.e., the number of times compounded per year can be defined with -c argument. + +``` +optional arguments: + --apr APR APR value in percentage to convert (default: 100) + -c COMPOUNDING, --compounding COMPOUNDING + Number of compounded periods in a year. 12 means compounding monthly (default: 12) + -n, --narrative Flag to show narra`ive instead of dataframe (default: False) + -h, --help show this help message (default: False) + --export EXPORT Export raw data into csv, json, xlsx (default: ) +``` diff --git a/website/content/terminal/cryptocurrency/tools/il/_index.md b/website/content/terminal/cryptocurrency/tools/il/_index.md new file mode 100644 index 00000000000..6cd557a64e7 --- /dev/null +++ b/website/content/terminal/cryptocurrency/tools/il/_index.md @@ -0,0 +1,20 @@ +`usage: il [-pcA PRICECHANGEA] [-pcB PRICECHANGEB] [-p PROPORTION] [-v VALUE] [-n] [-h] [--export EXPORT` + +Tool to calculate Impermanent Loss in a custom liquidity pool. Users can provide percentages increases for two tokens (and their weight in +the liquidity pool) and verify the impermanent loss that can occur. + +``` +optional arguments: + -pcA PRICECHANGEA, --priceChangeA PRICECHANGEA + Token A price change in percentage (default: 0) + -pcB PRICECHANGEB, --priceChangeB PRICECHANGEB + Token B price change in percentage (default: 100) + -p PROPORTION, --proportion PROPORTION + Pool proportion. E.g., 50 means that pool contains 50% of token A and 50% of token B, 30 means that pool contains + 30% of token A and 70% of token B (default: 50) + -v VALUE, --value VALUE + Initial amount of dollars that user provides to liquidity pool (default: 1000) + -n, --narrative Flag to show narrative instead of dataframe (default: False) + -h, --help show this help message (default: False) + --export EXPORT Export raw data into csv, json, xlsx (default: ) +``` diff --git a/website/data/menu/main.yml b/website/data/menu/main.yml index b333fedd579..c91e43a3b3a 100755 --- a/website/data/menu/main.yml +++ b/website/data/menu/main.yml @@ -666,6 +666,13 @@ main: ref: "/terminal/cryptocurrency/discovery/cpsearch" - name: cmctop ref: "/terminal/cryptocurrency/discovery/cmctop" + - name: tools + ref: "/terminal/cryptocurrency/tools" + sub: + - name: aprtoapy + ref: "/terminal/cryptocurrency/tools/aprtoapy" + - name: il + ref: "/terminal/cryptocurrency/tools/il" - name: overview ref: "/terminal/cryptocurrency/overview" sub: