示例#1
0
class HummingbotApplication(*commands):
    KILL_TIMEOUT = 10.0
    APP_WARNING_EXPIRY_DURATION = 3600.0
    APP_WARNING_STATUS_LIMIT = 6

    _main_app: Optional["HummingbotApplication"] = None

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global s_logger
        if s_logger is None:
            s_logger = logging.getLogger(__name__)
        return s_logger

    @classmethod
    def main_application(cls) -> "HummingbotApplication":
        if cls._main_app is None:
            cls._main_app = HummingbotApplication()
        return cls._main_app

    def __init__(self):
        # This is to start fetching trading pairs for auto-complete
        TradingPairFetcher.get_instance()
        self.ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
        self.markets: Dict[str, ExchangeBase] = {}
        # strategy file name and name get assigned value after import or create command
        self._strategy_file_name: str = None
        self.strategy_name: str = None
        self.strategy_task: Optional[asyncio.Task] = None
        self.strategy: Optional[StrategyBase] = None
        self.market_pair: Optional[CrossExchangeMarketPair] = None
        self.market_trading_pair_tuples: List[MarketTradingPairTuple] = []
        self.clock: Optional[Clock] = None
        self.market_trading_pairs_map = {}
        self.token_list = {}

        self.init_time: float = time.time()
        self.start_time: Optional[int] = None
        self.placeholder_mode = False
        self.log_queue_listener: Optional[
            logging.handlers.QueueListener] = None
        self.data_feed: Optional[DataFeedBase] = None
        self.notifiers: List[NotifierBase] = []
        self.kill_switch: Optional[KillSwitch] = None
        self._app_warnings: Deque[ApplicationWarning] = deque()
        self._trading_required: bool = True
        self._last_started_strategy_file: Optional[str] = None

        self.trade_fill_db: Optional[SQLConnectionManager] = None
        self.markets_recorder: Optional[MarketsRecorder] = None
        self._pmm_script_iterator = None
        self._binance_connector = None
        self._shared_client = None

        # gateway variables and monitor
        self._gateway_monitor = GatewayStatusMonitor(self)

        command_tabs = self.init_command_tabs()
        self.parser: ThrowingArgumentParser = load_parser(self, command_tabs)
        self.app = HummingbotCLI(input_handler=self._handle_command,
                                 bindings=load_key_bindings(self),
                                 completer=load_completer(self),
                                 command_tabs=command_tabs)

        self._init_gateway_monitor()

    @property
    def gateway_config_keys(self) -> List[str]:
        return self._gateway_monitor.gateway_config_keys

    @property
    def strategy_file_name(self) -> str:
        return self._strategy_file_name

    @strategy_file_name.setter
    def strategy_file_name(self, value: Optional[str]):
        self._strategy_file_name = value
        if value is not None:
            db_name = value.split(".")[0]
            self.trade_fill_db = SQLConnectionManager.get_trade_fills_instance(
                db_name)
        else:
            self.trade_fill_db = None

    @property
    def strategy_config_map(self):
        if self.strategy_name is not None:
            return get_strategy_config_map(self.strategy_name)
        return None

    def _init_gateway_monitor(self):
        try:
            # Do not start the gateway monitor during unit tests.
            if asyncio.get_running_loop() is not None:
                self._gateway_monitor = GatewayStatusMonitor(self)
                self._gateway_monitor.start()
        except RuntimeError:
            pass

    def notify(self, msg: str):
        self.app.log(msg)
        for notifier in self.notifiers:
            notifier.add_msg_to_queue(msg)

    def _handle_command(self, raw_command: str):
        # unset to_stop_config flag it triggered before loading any command
        if self.app.to_stop_config:
            self.app.to_stop_config = False

        raw_command = raw_command.strip()
        # NOTE: Only done for config command
        if raw_command.startswith("config"):
            command_split = raw_command.split(maxsplit=2)
        else:
            command_split = raw_command.split()
        try:
            if self.placeholder_mode:
                pass
            elif len(command_split) == 0:
                pass
            else:
                # Check if help is requested, if yes, print & terminate
                if len(command_split) > 1 and any(
                        arg in ["-h", "--help"] for arg in command_split[1:]):
                    self.help(raw_command)
                    return

                shortcuts = global_config_map.get("command_shortcuts").value
                shortcut = None
                # see if we match against shortcut command
                if shortcuts is not None:
                    for s in shortcuts:
                        if command_split[0] == s['command']:
                            shortcut = s
                            break

                # perform shortcut expansion
                if shortcut is not None:
                    # check number of arguments
                    num_shortcut_args = len(shortcut['arguments'])
                    if len(command_split) == num_shortcut_args + 1:
                        # notify each expansion if there's more than 1
                        verbose = True if len(
                            shortcut['output']) > 1 else False
                        # do argument replace and re-enter this function with the expanded command
                        for output_cmd in shortcut['output']:
                            final_cmd = output_cmd
                            for i in range(1, num_shortcut_args + 1):
                                final_cmd = final_cmd.replace(
                                    f'${i}', command_split[i])
                            if verbose is True:
                                self.notify(f'  >>> {final_cmd}')
                            self._handle_command(final_cmd)
                    else:
                        self.notify('Invalid number of arguments for shortcut')
                # regular command
                else:
                    args = self.parser.parse_args(args=command_split)
                    kwargs = vars(args)
                    if not hasattr(args, "func"):
                        self.app.handle_tab_command(self, command_split[0],
                                                    kwargs)
                    else:
                        f = args.func
                        del kwargs["func"]
                        f(**kwargs)
        except ArgumentParserError as e:
            if not self.be_silly(raw_command):
                self.notify(str(e))
        except NotImplementedError:
            self.notify(
                "Command not yet implemented. This feature is currently under development."
            )
        except Exception as e:
            self.logger().error(e, exc_info=True)

    async def _cancel_outstanding_orders(self) -> bool:
        success = True
        try:
            kill_timeout: float = self.KILL_TIMEOUT
            self.notify("Canceling outstanding orders...")

            for market_name, market in self.markets.items():
                cancellation_results = await market.cancel_all(kill_timeout)
                uncancelled = list(
                    filter(lambda cr: cr.success is False,
                           cancellation_results))
                if len(uncancelled) > 0:
                    success = False
                    uncancelled_order_ids = list(
                        map(lambda cr: cr.order_id, uncancelled))
                    self.notify(
                        "\nFailed to cancel the following orders on %s:\n%s" %
                        (market_name, '\n'.join(uncancelled_order_ids)))
        except Exception:
            self.logger().error("Error canceling outstanding orders.",
                                exc_info=True)
            success = False

        if success:
            self.notify("All outstanding orders canceled.")
        return success

    async def run(self):
        await self.app.run()

    def add_application_warning(self, app_warning: ApplicationWarning):
        self._expire_old_application_warnings()
        self._app_warnings.append(app_warning)

    def clear_application_warning(self):
        self._app_warnings.clear()

    @staticmethod
    def _initialize_market_assets(
            market_name: str,
            trading_pairs: List[str]) -> List[Tuple[str, str]]:
        market_trading_pairs: List[Tuple[str, str]] = [
            (trading_pair.split('-')) for trading_pair in trading_pairs
        ]
        return market_trading_pairs

    def _initialize_markets(self, market_names: List[Tuple[str, List[str]]]):
        # aggregate trading_pairs if there are duplicate markets

        for market_name, trading_pairs in market_names:
            if market_name not in self.market_trading_pairs_map:
                self.market_trading_pairs_map[market_name] = []
            for hb_trading_pair in trading_pairs:
                self.market_trading_pairs_map[market_name].append(
                    hb_trading_pair)

        for connector_name, trading_pairs in self.market_trading_pairs_map.items(
        ):
            conn_setting = AllConnectorSettings.get_connector_settings(
            )[connector_name]

            if connector_name.endswith(
                    "paper_trade"
            ) and conn_setting.type == ConnectorType.Exchange:
                connector = create_paper_trade_market(conn_setting.parent_name,
                                                      trading_pairs)
                paper_trade_account_balance = global_config_map.get(
                    "paper_trade_account_balance").value
                if paper_trade_account_balance is not None:
                    for asset, balance in paper_trade_account_balance.items():
                        connector.set_balance(asset, balance)
            else:
                Security.update_config_map(global_config_map)
                keys = {
                    key: config.value
                    for key, config in global_config_map.items()
                    if key in conn_setting.config_keys
                }
                init_params = conn_setting.conn_init_parameters(keys)
                init_params.update(trading_pairs=trading_pairs,
                                   trading_required=self._trading_required)
                connector_class = get_connector_class(connector_name)
                connector = connector_class(**init_params)
            self.markets[connector_name] = connector

        self.markets_recorder = MarketsRecorder(
            self.trade_fill_db,
            list(self.markets.values()),
            self.strategy_file_name,
            self.strategy_name,
        )
        self.markets_recorder.start()

    def _initialize_notifiers(self):
        if global_config_map.get("telegram_enabled").value:
            # TODO: refactor to use single instance
            if not any(
                [isinstance(n, TelegramNotifier) for n in self.notifiers]):
                self.notifiers.append(
                    TelegramNotifier(
                        token=global_config_map["telegram_token"].value,
                        chat_id=global_config_map["telegram_chat_id"].value,
                        hb=self,
                    ))
        for notifier in self.notifiers:
            notifier.start()

    def init_command_tabs(self) -> Dict[str, CommandTab]:
        """
        Initiates and returns a CommandTab dictionary with mostly defaults and None values, These values will be
        populated later on by HummingbotCLI
        """
        command_tabs: Dict[str, CommandTab] = {}
        for tab_class in tab_classes:
            name = tab_class.get_command_name()
            command_tabs[name] = CommandTab(name, None, None, None, tab_class)
        return command_tabs
class HummingbotCLITest(unittest.TestCase):
    command_name = "command_1"

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.ev_loop.run_until_complete(read_system_configs_from_yml())

    def setUp(self) -> None:
        super().setUp()

        tabs = {self.command_name: CommandTab(self.command_name, None, None, None, MagicMock())}
        self.mock_hb = MagicMock()
        self.app = HummingbotCLI(None, None, None, tabs)
        self.app.app = MagicMock()

    def test_handle_tab_command_on_close_argument(self):
        tab = self.app.command_tabs[self.command_name]
        tab.close_button = MagicMock()
        tab.button = MagicMock()
        tab.output_field = MagicMock()
        self.app.handle_tab_command(self.mock_hb, self.command_name, {"close": True})
        self.assertIsNone(tab.button)
        self.assertIsNone(tab.close_button)
        self.assertIsNone(tab.output_field)
        self.assertFalse(tab.is_selected)
        self.assertEqual(tab.tab_index, 0)

    def test_handle_tab_command_create_new_tab_and_display(self):
        tab = self.app.command_tabs[self.command_name]
        self.app.handle_tab_command(self.mock_hb, self.command_name, {"close": False})
        self.assertIsInstance(tab.button, Button)
        self.assertIsInstance(tab.close_button, Button)
        self.assertIsInstance(tab.output_field, CustomTextArea)
        self.assertEqual(tab.tab_index, 1)
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.called)

    @patch("hummingbot.client.ui.layout.Layout")
    @patch("hummingbot.client.ui.layout.FloatContainer")
    @patch("hummingbot.client.ui.layout.ConditionalContainer")
    @patch("hummingbot.client.ui.layout.Box")
    @patch("hummingbot.client.ui.layout.HSplit")
    @patch("hummingbot.client.ui.layout.VSplit")
    def test_handle_tab_command_on_existing_tab(self, mock_vsplit, mock_hsplit, mock_box, moc_cc, moc_fc, mock_layout):
        tab = self.app.command_tabs[self.command_name]
        tab.button = MagicMock()
        tab.output_field = MagicMock()
        tab.close_button = MagicMock()
        tab.is_selected = False
        self.app.handle_tab_command(self.mock_hb, self.command_name, {"close": False})
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.call_count == 1)

        # Test display not called if there is a running task
        tab.is_selected = False
        tab.task = MagicMock()
        tab.task.done.return_value = False
        self.app.handle_tab_command(self.mock_hb, self.command_name, {"close": False})
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.call_count == 1)

    @patch("hummingbot.client.ui.layout.Layout")
    @patch("hummingbot.client.ui.layout.FloatContainer")
    @patch("hummingbot.client.ui.layout.ConditionalContainer")
    @patch("hummingbot.client.ui.layout.Box")
    @patch("hummingbot.client.ui.layout.HSplit")
    @patch("hummingbot.client.ui.layout.VSplit")
    def test_tab_navigation(self, mock_vsplit, mock_hsplit, mock_box, moc_cc, moc_fc, mock_layout):
        tab2 = CommandTab("command_2", None, None, None, MagicMock(), False)

        self.app.command_tabs["command_2"] = tab2
        tab1 = self.app.command_tabs[self.command_name]

        self.app.handle_tab_command(self.mock_hb, self.command_name, {"close": False})
        self.app.handle_tab_command(self.mock_hb, "command_2", {"close": False})
        self.assertTrue(tab2.is_selected)

        self.app.tab_navigate_left()
        self.assertTrue(tab1.is_selected)
        self.assertFalse(tab2.is_selected)
        self.app.tab_navigate_left()
        self.assertTrue(all(not t.is_selected for t in self.app.command_tabs.values()))
        self.app.tab_navigate_left()
        self.assertTrue(all(not t.is_selected for t in self.app.command_tabs.values()))

        self.app.tab_navigate_right()
        self.assertTrue(tab1.is_selected)

        self.app.tab_navigate_right()
        self.assertFalse(tab1.is_selected)
        self.assertTrue(tab2.is_selected)

        self.app.tab_navigate_right()
        self.assertFalse(tab1.is_selected)
        self.assertTrue(tab2.is_selected)

    @patch("hummingbot.client.ui.hummingbot_cli.global_config_map")
    @patch("hummingbot.client.ui.hummingbot_cli.init_logging")
    def test_did_start_ui(self, mock_init_logging: MagicMock, mock_config_map: MagicMock):
        class UIStartHandler(EventListener):
            def __init__(self):
                super().__init__()
                self.mock = MagicMock()

            def __call__(self, _):
                self.mock()

        handler: UIStartHandler = UIStartHandler()
        self.app.add_listener(HummingbotUIEvent.Start, handler)
        self.app.did_start_ui()

        mock_config_map.get.assert_called()
        mock_init_logging.assert_called()
        handler.mock.assert_called()
class HummingbotCLITest(unittest.TestCase):
    command_name = "command_1"

    def setUp(self) -> None:
        super().setUp()

        tabs = {
            self.command_name:
            CommandTab(self.command_name, None, None, None, MagicMock())
        }
        self.mock_hb = MagicMock()
        self.app = HummingbotCLI(None, None, None, tabs)
        self.app.app = MagicMock()

    def test_handle_tab_command_on_close_argument(self):
        tab = self.app.command_tabs[self.command_name]
        tab.close_button = MagicMock()
        tab.button = MagicMock()
        tab.output_field = MagicMock()
        self.app.handle_tab_command(self.mock_hb, self.command_name,
                                    {"close": True})
        self.assertIsNone(tab.button)
        self.assertIsNone(tab.close_button)
        self.assertIsNone(tab.output_field)
        self.assertFalse(tab.is_selected)
        self.assertEqual(tab.tab_index, 0)

    def test_handle_tab_command_create_new_tab_and_display(self):
        tab = self.app.command_tabs[self.command_name]
        self.app.handle_tab_command(self.mock_hb, self.command_name,
                                    {"close": False})
        self.assertIsInstance(tab.button, Button)
        self.assertIsInstance(tab.close_button, Button)
        self.assertIsInstance(tab.output_field, CustomTextArea)
        self.assertEqual(tab.tab_index, 1)
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.called)

    @patch("hummingbot.client.ui.layout.Layout")
    @patch("hummingbot.client.ui.layout.FloatContainer")
    @patch("hummingbot.client.ui.layout.ConditionalContainer")
    @patch("hummingbot.client.ui.layout.Box")
    @patch("hummingbot.client.ui.layout.HSplit")
    @patch("hummingbot.client.ui.layout.VSplit")
    def test_handle_tab_command_on_existing_tab(self, mock_vsplit, mock_hsplit,
                                                mock_box, moc_cc, moc_fc,
                                                mock_layout):
        tab = self.app.command_tabs[self.command_name]
        tab.button = MagicMock()
        tab.output_field = MagicMock()
        tab.close_button = MagicMock()
        tab.is_selected = False
        self.app.handle_tab_command(self.mock_hb, self.command_name,
                                    {"close": False})
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.call_count == 1)

        # Test display not called if there is a running task
        tab.is_selected = False
        tab.task = MagicMock()
        tab.task.done.return_value = False
        self.app.handle_tab_command(self.mock_hb, self.command_name,
                                    {"close": False})
        self.assertTrue(tab.is_selected)
        self.assertTrue(tab.tab_class.display.call_count == 1)

    @patch("hummingbot.client.ui.layout.Layout")
    @patch("hummingbot.client.ui.layout.FloatContainer")
    @patch("hummingbot.client.ui.layout.ConditionalContainer")
    @patch("hummingbot.client.ui.layout.Box")
    @patch("hummingbot.client.ui.layout.HSplit")
    @patch("hummingbot.client.ui.layout.VSplit")
    def test_tab_navigation(self, mock_vsplit, mock_hsplit, mock_box, moc_cc,
                            moc_fc, mock_layout):
        tab2 = CommandTab("command_2", None, None, None, MagicMock(), False)

        self.app.command_tabs["command_2"] = tab2
        tab1 = self.app.command_tabs[self.command_name]

        self.app.handle_tab_command(self.mock_hb, self.command_name,
                                    {"close": False})
        self.app.handle_tab_command(self.mock_hb, "command_2",
                                    {"close": False})
        self.assertTrue(tab2.is_selected)

        self.app.tab_navigate_left()
        self.assertTrue(tab1.is_selected)
        self.assertFalse(tab2.is_selected)
        self.app.tab_navigate_left()
        self.assertTrue(
            all(not t.is_selected for t in self.app.command_tabs.values()))
        self.app.tab_navigate_left()
        self.assertTrue(
            all(not t.is_selected for t in self.app.command_tabs.values()))

        self.app.tab_navigate_right()
        self.assertTrue(tab1.is_selected)

        self.app.tab_navigate_right()
        self.assertFalse(tab1.is_selected)
        self.assertTrue(tab2.is_selected)

        self.app.tab_navigate_right()
        self.assertFalse(tab1.is_selected)
        self.assertTrue(tab2.is_selected)