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)