def test_get_binance_prices(self, mock_api): url = RateOracle.binance_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.Binance), repeat=True) url = RateOracle.binance_us_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Fixture.Binance mock_api.get(regex_url, body=json.dumps(Fixture.BinanceUS), repeat=True) com_prices = self.async_run_with_timeout( RateOracle.get_binance_prices_by_domain( RateOracle.binance_price_url)) self._assert_rate_dict(com_prices) us_prices = self.async_run_with_timeout( RateOracle.get_binance_prices_by_domain( RateOracle.binance_us_price_url, "USD", domain="us")) self._assert_rate_dict(us_prices) self.assertGreater(len(us_prices), 1) quotes = {p.split("-")[1] for p in us_prices} self.assertEqual(len(quotes), 1) self.assertEqual(list(quotes)[0], "USD") combined_prices = self.async_run_with_timeout( RateOracle.get_binance_prices()) self._assert_rate_dict(combined_prices) self.assertGreater(len(combined_prices), len(com_prices))
def test_calculate_fees_in_quote_for_one_trade_with_fees_different_tokens( self): rate_oracle = RateOracle() rate_oracle._prices["DAI-COINALPHA"] = Decimal("2") rate_oracle._prices["USDT-DAI"] = Decimal("0.9") RateOracle._shared_instance = rate_oracle performance_metric = PerformanceMetrics() flat_fees = [ TokenAmount(token="USDT", amount=Decimal("10")), TokenAmount(token="DAI", amount=Decimal("5")), ] trade = Trade(trading_pair="HBOT-COINALPHA", side=TradeType.BUY, price=Decimal("1000"), amount=Decimal("1"), order_type=OrderType.LIMIT, market="binance", timestamp=1640001112.223, trade_fee=AddedToCostTradeFee(percent=Decimal("0.1"), percent_token="COINALPHA", flat_fees=flat_fees)) self.async_run_with_timeout( performance_metric._calculate_fees(quote="COINALPHA", trades=[trade])) expected_fee_amount = trade.amount * trade.price * trade.trade_fee.percent expected_fee_amount += flat_fees[0].amount * Decimal("0.9") * Decimal( "2") expected_fee_amount += flat_fees[1].amount * Decimal("2") self.assertEqual(expected_fee_amount, performance_metric.fee_in_quote)
def test_get_rate_coingecko(self, mock_api): url = RateOracle.coingecko_usd_price_url.format(1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format(2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format(3, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoPage3), repeat=True) url = RateOracle.coingecko_usd_price_url.format(4, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoPage4), repeat=True) url = RateOracle.coingecko_supported_vs_tokens_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoVSCurrencies), repeat=True) rates = self.async_run_with_timeout(RateOracle.get_coingecko_prices_by_page("USD", 1)) self._assert_rate_dict(rates) rates = self.async_run_with_timeout(RateOracle.get_coingecko_prices("USD")) self._assert_rate_dict(rates)
def get_taker_to_maker_conversion_rate( self, market_pair: CrossExchangeMarketPair ) -> Tuple[str, str, Decimal, str, str, Decimal]: """ Find conversion rates from taker market to maker market :param market_pair: maker and taker trading pairs for which to do conversion :return: A tuple of quote pair symbol, quote conversion rate source, quote conversion rate, base pair symbol, base conversion rate source, base conversion rate """ quote_pair = f"{market_pair.taker.quote_asset}-{market_pair.maker.quote_asset}" if market_pair.taker.quote_asset != market_pair.maker.quote_asset: quote_rate_source = RateOracle.source.name quote_rate = RateOracle.get_instance().rate(quote_pair) else: quote_rate_source = "fixed" quote_rate = Decimal("1") base_pair = f"{market_pair.taker.base_asset}-{market_pair.maker.base_asset}" if market_pair.taker.base_asset != market_pair.maker.base_asset: base_rate_source = RateOracle.source.name base_rate = RateOracle.get_instance().rate(base_pair) else: base_rate_source = "fixed" base_rate = Decimal("1") return quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate
async def start_check( self, # type: HummingbotApplication log_level: Optional[str] = None, restore: Optional[bool] = False): if self.strategy_task is not None and not self.strategy_task.done(): self._notify( 'The bot is already running - please run "stop" first') return if settings.required_rate_oracle: if not (await self.confirm_oracle_conversion_rate()): self._notify("The strategy failed to start.") return else: RateOracle.get_instance().start() is_valid = await self.status_check_all(notify_success=False) if not is_valid: self._notify("Status checks failed. Start aborted.") return if self._last_started_strategy_file != self.strategy_file_name: init_logging( "hummingbot_logs.yml", override_log_level=log_level.upper() if log_level else None, strategy_file_path=self.strategy_file_name) self._last_started_strategy_file = self.strategy_file_name # If macOS, disable App Nap. if platform.system() == "Darwin": import appnope appnope.nope() self._initialize_notifiers() self._notify( f"\nStatus check complete. Starting '{self.strategy_name}' strategy..." ) if global_config_map.get("paper_trade_enabled").value: self._notify( "\nPaper Trading ON: All orders are simulated, and no real orders are placed." ) for exchange in settings.required_exchanges: connector = str(exchange) status = get_connector_status(connector) # Display custom warning message for specific connectors warning_msg = warning_messages.get(connector, None) if warning_msg is not None: self._notify(f"\nConnector status: {status}\n" f"{warning_msg}") # Display warning message if the exchange connector has outstanding issues or not working elif status != "GREEN": self._notify( f"\nConnector status: {status}. This connector has one or more issues.\n" "Refer to our Github page for more info: https://github.com/coinalpha/hummingbot" ) await self.start_market_making(self.strategy_name, restore)
def test_performance_metrics(self): rate_oracle = RateOracle() rate_oracle._prices["USDT-HBOT"] = Decimal("5") RateOracle._shared_instance = rate_oracle trade_fee = AddedToCostTradeFee( flat_fees=[TokenAmount(quote, Decimal("0"))]) trades = [ TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol=trading_pair, base_asset=base, quote_asset=quote, timestamp=int(time.time()), order_id="someId0", trade_type="BUY", order_type="LIMIT", price=100, amount=10, trade_fee=trade_fee.to_json(), exchange_trade_id="someExchangeId0", position=PositionAction.NIL.value, ), TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol=trading_pair, base_asset=base, quote_asset=quote, timestamp=int(time.time()), order_id="someId1", trade_type="SELL", order_type="LIMIT", price=120, amount=15, trade_fee=trade_fee.to_json(), exchange_trade_id="someExchangeId1", position=PositionAction.NIL.value, ) ] cur_bals = {base: 100, quote: 10000} metrics = asyncio.get_event_loop().run_until_complete( PerformanceMetrics.create(trading_pair, trades, cur_bals)) self.assertEqual(Decimal("799"), metrics.trade_pnl) print(metrics)
def calculate_profitability(self, position: UniswapV3InFlightPosition): """ Does simple computation and returns a dictionary containing data required by other functions. :param position: an instance of UniswapV3InFlightPosition :return {}: dictionaty containing "base_change", "quote_change", "base_fee", "quote_fee" "tx_fee", "profitability". """ base_tkn, quote_tkn = position.trading_pair.split("-") init_base = Decimal(str(position.base_amount)) init_quote = Decimal(str(position.quote_amount)) base_change = Decimal(str(position.current_base_amount)) - Decimal( str(position.base_amount)) quote_change = Decimal(str(position.current_quote_amount)) - Decimal( str(position.quote_amount)) base_fee = Decimal(str(position.unclaimed_base_amount)) quote_fee = Decimal(str(position.unclaimed_quote_amount)) if base_tkn != "WETH": fee_rate = RateOracle.get_instance().rate(f"ETH-{quote_tkn}") if fee_rate: tx_fee = Decimal(str(position.gas_price)) * 2 * fee_rate else: # cases like this would be rare tx_fee = Decimal(str(position.gas_price)) * 2 else: tx_fee = Decimal(str(position.gas_price)) * 2 init_value = (init_base * self._last_price) + init_quote profitability = s_decimal_0 if init_value == s_decimal_0 else \ ((((base_change + base_fee) * self._last_price) + quote_change + quote_fee - tx_fee) / init_value) return { "base_change": base_change, "quote_change": quote_change, "base_fee": base_fee, "quote_fee": quote_fee, "tx_fee": tx_fee, "profitability": profitability }
def test_get_ascend_ex_prices(self, mock_api): url = RateOracle.ascend_ex_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.AscendEx), repeat=True) prices = self.async_run_with_timeout(RateOracle.get_ascend_ex_prices()) self._assert_rate_dict(prices)
def setUp(self) -> None: super().setUp() self.metrics_collector_url = "localhost" self.connector_name = "test_connector" self.instance_id = "test_instance_id" self.client_version = "0.1" self.rate_oracle = RateOracle() self.connector_mock = MagicMock() type(self.connector_mock).name = PropertyMock( return_value=self.connector_name) self.dispatcher_mock = MagicMock() type(self.dispatcher_mock).log_server_url = PropertyMock( return_value=self.metrics_collector_url) self.original_client_version = hummingbot.connector.connector_metrics_collector.CLIENT_VERSION hummingbot.connector.connector_metrics_collector.CLIENT_VERSION = self.client_version self.metrics_collector = TradeVolumeMetricCollector( connector=self.connector_mock, activation_interval=Decimal(10), rate_provider=self.rate_oracle, instance_id=self.instance_id) self.metrics_collector._dispatcher = self.dispatcher_mock
async def stop_loop( self, # type: HummingbotApplication skip_order_cancellation: bool = False): self.logger().info("stop command initiated.") self._notify("\nWinding down...") # Restore App Nap on macOS. if platform.system() == "Darwin": import appnope appnope.nap() if self._script_iterator is not None: self._script_iterator.stop(self.clock) if self._trading_required and not skip_order_cancellation: # Remove the strategy from clock before cancelling orders, to # prevent race condition where the strategy tries to create more # orders during cancellation. if self.clock: self.clock.remove_iterator(self.strategy) success = await self._cancel_outstanding_orders() # Give some time for cancellation events to trigger await asyncio.sleep(0.5) if success: # Only erase markets when cancellation has been successful self.markets = {} if self.strategy_task is not None and not self.strategy_task.cancelled( ): self.strategy_task.cancel() if RateOracle.get_instance().started: RateOracle.get_instance().stop() if self.markets_recorder is not None: self.markets_recorder.stop() if self.kill_switch is not None: self.kill_switch.stop() self.strategy_task = None self.strategy = None self.market_pair = None self.clock = None self.markets_recorder = None self.market_trading_pairs_map.clear()
def _get_exchange_rate(trading_pair: str, exchange: Optional["ExchangeBase"] = None) -> Decimal: from hummingbot.core.rate_oracle.rate_oracle import RateOracle if exchange is not None: rate = exchange.get_price(trading_pair, is_buy=True) else: rate = RateOracle.get_instance().rate(trading_pair) return rate
def test_get_kucoin_prices(self, mock_api, connector_creator_mock): connector_creator_mock.side_effect = [self._kucoin_connector] url = RateOracle.kucoin_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.Kucoin), repeat=True) prices = self.async_run_with_timeout(RateOracle.get_kucoin_prices()) self._assert_rate_dict(prices)
def init_params( self, market_info_1: MarketTradingPairTuple, market_info_2: MarketTradingPairTuple, min_profitability: Decimal, order_amount: Decimal, market_1_slippage_buffer: Decimal = Decimal("0"), market_2_slippage_buffer: Decimal = Decimal("0"), concurrent_orders_submission: bool = True, status_report_interval: float = 900, gateway_transaction_cancel_interval: int = 600, ): """ Assigns strategy parameters, this function must be called directly after init. The reason for this is to make the parameters discoverable on introspect (it is not possible on init of a Cython class). :param market_info_1: The first market :param market_info_2: The second market :param min_profitability: The minimum profitability for execute trades (e.g. 0.0003 for 0.3%) :param order_amount: The order amount :param market_1_slippage_buffer: The buffer for which to adjust order price for higher chance of the order getting filled. This is quite important for AMM which transaction takes a long time where a slippage is acceptable rather having the transaction get rejected. The submitted order price will be adjust higher for buy order and lower for sell order. :param market_2_slippage_buffer: The slipper buffer for market_2 :param concurrent_orders_submission: whether to submit both arbitrage taker orders (buy and sell) simultaneously If false, the bot will wait for first exchange order filled before submitting the other order. :param status_report_interval: Amount of seconds to wait to refresh the status report :param gateway_transaction_cancel_interval: Amount of seconds to wait before trying to cancel orders that are blockchain transactions that have not been included in a block (they are still in the mempool). """ self._market_info_1 = market_info_1 self._market_info_2 = market_info_2 self._min_profitability = min_profitability self._order_amount = order_amount self._market_1_slippage_buffer = market_1_slippage_buffer self._market_2_slippage_buffer = market_2_slippage_buffer self._concurrent_orders_submission = concurrent_orders_submission self._last_no_arb_reported = 0 self._all_arb_proposals = None self._all_markets_ready = False self._ev_loop = asyncio.get_event_loop() self._main_task = None self._last_timestamp = 0 self._status_report_interval = status_report_interval self.add_markets([market_info_1.market, market_info_2.market]) self._quote_eth_rate_fetch_loop_task = None self._rate_source = RateOracle.get_instance() self._cancel_outdated_orders_task = None self._gateway_transaction_cancel_interval = gateway_transaction_cancel_interval self._order_id_side_map: Dict[str, ArbProposalSide] = {}
def test_rate_oracle_network(self): oracle = RateOracle.get_instance() oracle.start() asyncio.get_event_loop().run_until_complete(oracle.get_ready()) self.assertGreater(len(oracle.prices), 0) rate = oracle.rate("SCRT-USDT") self.assertGreater(rate, 0) rate1 = oracle.rate("BTC-USDT") self.assertGreater(rate1, 100) oracle.stop()
def test_calculate_fees_in_quote_for_one_trade_fill_with_fees_different_tokens( self): rate_oracle = RateOracle() rate_oracle._prices["DAI-COINALPHA"] = Decimal("2") rate_oracle._prices["USDT-DAI"] = Decimal("0.9") RateOracle._shared_instance = rate_oracle performance_metric = PerformanceMetrics() flat_fees = [ TokenAmount(token="USDT", amount=Decimal("10")), TokenAmount(token="DAI", amount=Decimal("5")), ] trade = TradeFill( config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol="HBOT-COINALPHA", base_asset="HBOT", quote_asset="COINALPHA", timestamp=int(time.time()), order_id="someId0", trade_type="BUY", order_type="LIMIT", price=1000, amount=1, trade_fee=AddedToCostTradeFee(percent=Decimal("0.1"), percent_token="COINALPHA", flat_fees=flat_fees).to_json(), exchange_trade_id="someExchangeId0", position=PositionAction.NIL.value, ) self.async_run_with_timeout( performance_metric._calculate_fees(quote="COINALPHA", trades=[trade])) expected_fee_amount = Decimal(str(trade.amount)) * Decimal( str(trade.price)) * Decimal("0.1") expected_fee_amount += flat_fees[0].amount * Decimal("0.9") * Decimal( "2") expected_fee_amount += flat_fees[1].amount * Decimal("2") self.assertEqual(expected_fee_amount, performance_metric.fee_in_quote)
def setUpClass(cls): super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.client_config_map = ClientConfigAdapter(ClientConfigMap()) cls._binance_connector = RateOracle._binance_connector_without_private_keys( domain="com") cls._binance_connector._set_trading_pair_symbol_map( bidict({ "ETHBTC": "ETH-BTC", "LTCBTC": "LTC-BTC", "BTCUSDT": "BTC-USDT", "SCRTBTC": "SCRT-BTC" })) cls._binance_connector_us = RateOracle._binance_connector_without_private_keys( domain="us") cls._binance_connector_us._set_trading_pair_symbol_map( bidict({ "BTCUSD": "BTC-USD", "ETHUSD": "ETH-USD" })) AscendExAPIOrderBookDataSource._trading_pair_symbol_map = bidict({ "ETH/BTC": "ETH-BTC", "LTC/BTC": "LTC-BTC", "BTC/USDT": "BTC-USDT", "SCRT/BTC": "SCRT-BTC", "MAPS/USDT": "MAPS-USDT", "QTUM/BTC": "QTUM-BTC" }) cls._kucoin_connector = RateOracle._kucoin_connector_without_private_keys( ) cls._kucoin_connector._set_trading_pair_symbol_map( bidict({ "SHA-USDT": "SHA-USDT", "LOOM-BTC": "LOOM-BTC", }))
def test_rate_oracle_network(self, mock_api): url = RateOracle.binance_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.Binance)) url = RateOracle.binance_us_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Fixture.Binance mock_api.get(regex_url, body=json.dumps(Fixture.BinanceUS)) oracle = RateOracle() oracle.start() self.async_run_with_timeout(oracle.get_ready()) self.assertGreater(len(oracle.prices), 0) rate = oracle.rate("SCRT-USDT") self.assertGreater(rate, 0) rate1 = oracle.rate("BTC-USDT") self.assertGreater(rate1, 100) oracle.stop()
def test_find_rate_from_source(self, mock_api): url = RateOracle.binance_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.Binance), repeat=True) url = RateOracle.binance_us_price_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Fixture.Binance mock_api.get(regex_url, body=json.dumps(Fixture.BinanceUS), repeat=True) expected_rate = (Decimal("33327.43000000") + Decimal("33327.44000000")) / Decimal(2) rate = self.async_run_with_timeout(RateOracle.rate_async("BTC-USDT")) self.assertEqual(expected_rate, rate)
def _get_exchange_rate(trading_pair: str, exchange: Optional["ExchangeBase"] = None, rate_source: Optional[Any] = None) -> Decimal: from hummingbot.core.rate_oracle.rate_oracle import RateOracle if exchange is not None and trading_pair in exchange.order_books: rate = exchange.get_price_by_type(trading_pair, PriceType.MidPrice) else: local_rate_source = rate_source or RateOracle.get_instance() rate = local_rate_source.rate(trading_pair) if rate is None: raise ValueError( f"Could not find the exchange rate for {trading_pair} using the rate source " f"{local_rate_source} (please verify it has been correctly configured)" ) return rate
def create_proposal(self) -> List[OrderCandidate]: """ Creates and returns a proposal (a list of order candidate), in this strategy the list has 1 element at most. """ daily_closes = self._get_daily_close_list(self.trading_pair) start_index = (-1 * self.moving_avg_period) - 1 # Calculate the average of the 50 element prior to the last element avg_close = mean(daily_closes[start_index:-1]) proposal = [] # If the current price (the last close) is below the dip, add a new order candidate to the proposal if daily_closes[-1] < avg_close * (Decimal("1") - self.dip_percentage): order_price = self.connector.get_price(self.trading_pair, False) * Decimal("0.9") usd_conversion_rate = RateOracle.get_instance().rate(self.conversion_pair) amount = (self.buy_usd_amount / usd_conversion_rate) / order_price proposal.append(OrderCandidate(self.trading_pair, False, OrderType.LIMIT, TradeType.BUY, amount, order_price)) return proposal
def test_rate_oracle_network(self): oracle = RateOracle.get_instance() oracle.start() asyncio.get_event_loop().run_until_complete(oracle.get_ready()) print(oracle.prices) self.assertGreater(len(oracle.prices), 0) rate = oracle.rate("SCRT-USDT") print(f"rate SCRT-USDT: {rate}") self.assertGreater(rate, 0) rate1 = oracle.rate("BTC-USDT") print(f"rate BTC-USDT: {rate1}") self.assertGreater(rate1, 100) # wait for 5 s to check rate again asyncio.get_event_loop().run_until_complete(asyncio.sleep(5)) rate2 = oracle.rate("BTC-USDT") print(f"rate BTC-USDT: {rate2}") self.assertNotEqual(0, rate2) oracle.stop()
async def calculate_profitability(self, position: UniswapV3InFlightPosition): """ Does simple computation and returns a dictionary containing data required by other functions. :param position: an instance of UniswapV3InFlightPosition :return {}: dictionaty containing "base_change", "quote_change", "base_fee", "quote_fee" "tx_fee", "profitability". """ base_tkn, quote_tkn = position.trading_pair.split("-") init_base = position.base_amount init_quote = position.quote_amount base_change = position.current_base_amount - position.base_amount quote_change = position.current_quote_amount - position.quote_amount base_fee = position.unclaimed_base_amount quote_fee = position.unclaimed_quote_amount if len(position.tx_fees) < 2 or position.tx_fees[-1] == s_decimal_0: remove_lp_fee = await self._market_info.market._remove_position( position.hb_id, position.token_id, Decimal("100.0"), True) remove_lp_fee = remove_lp_fee if remove_lp_fee is not None else s_decimal_0 position.tx_fees.append(remove_lp_fee) if quote_tkn != "WETH": fee_rate = RateOracle.get_instance().rate(f"ETH-{quote_tkn}") if fee_rate: tx_fee = sum(position.tx_fees) * fee_rate else: # cases like this would be rare tx_fee = sum(position.tx_fees) else: tx_fee = sum(position.tx_fees) init_value = (init_base * self._last_price) + init_quote profitability = s_decimal_0 if init_value == s_decimal_0 else \ ((((base_change + base_fee) * self._last_price) + quote_change + quote_fee - tx_fee) / init_value) return { "base_change": base_change, "quote_change": quote_change, "base_fee": base_fee, "quote_fee": quote_fee, "tx_fee": tx_fee, "profitability": profitability }
def test_performance_metrics_for_derivatives(self, is_trade_fill_mock): rate_oracle = RateOracle() rate_oracle._prices["USDT-HBOT"] = Decimal("5") RateOracle._shared_instance = rate_oracle is_trade_fill_mock.return_value = True trades = [] trades.append( self.mock_trade(id="order1", amount=Decimal("100"), price=Decimal("10"), position="OPEN", type="BUY", fee=AddedToCostTradeFee( flat_fees=[TokenAmount(quote, Decimal("0"))]))) trades.append( self.mock_trade(id="order2", amount=Decimal("100"), price=Decimal("15"), position="CLOSE", type="SELL", fee=AddedToCostTradeFee( flat_fees=[TokenAmount(quote, Decimal("0"))]))) trades.append( self.mock_trade(id="order3", amount=Decimal("100"), price=Decimal("20"), position="OPEN", type="SELL", fee=AddedToCostTradeFee( Decimal("0.1"), flat_fees=[TokenAmount("USD", Decimal("0"))]))) trades.append( self.mock_trade(id="order4", amount=Decimal("100"), price=Decimal("15"), position="CLOSE", type="BUY", fee=AddedToCostTradeFee( Decimal("0.1"), flat_fees=[TokenAmount("USD", Decimal("0"))]))) cur_bals = {base: 100, quote: 10000} metrics = asyncio.get_event_loop().run_until_complete( PerformanceMetrics.create(trading_pair, trades, cur_bals)) self.assertEqual(metrics.num_buys, 2) self.assertEqual(metrics.num_sells, 2) self.assertEqual(metrics.num_trades, 4) self.assertEqual(metrics.b_vol_base, Decimal("200")) self.assertEqual(metrics.s_vol_base, Decimal("-200")) self.assertEqual(metrics.tot_vol_base, Decimal("0")) self.assertEqual(metrics.b_vol_quote, Decimal("-2500")) self.assertEqual(metrics.s_vol_quote, Decimal("3500")) self.assertEqual(metrics.tot_vol_quote, Decimal("1000")) self.assertEqual(metrics.avg_b_price, Decimal("12.5")) self.assertEqual(metrics.avg_s_price, Decimal("17.5")) self.assertEqual(metrics.avg_tot_price, Decimal("15")) self.assertEqual(metrics.start_base_bal, Decimal("100")) self.assertEqual(metrics.start_quote_bal, Decimal("9000")) self.assertEqual(metrics.cur_base_bal, 100) self.assertEqual(metrics.cur_quote_bal, 10000), self.assertEqual(metrics.start_price, Decimal("10")), self.assertEqual(metrics.cur_price, Decimal("0.2")) self.assertEqual(metrics.trade_pnl, Decimal("1000")) self.assertEqual(metrics.total_pnl, Decimal("650"))
async def start_check(self, # type: HummingbotApplication log_level: Optional[str] = None, restore: Optional[bool] = False, strategy_file_name: Optional[str] = None): if self.strategy_task is not None and not self.strategy_task.done(): self.notify('The bot is already running - please run "stop" first') return if settings.required_rate_oracle: # If the strategy to run requires using the rate oracle to find FX rates, validate there is a rate for # each configured token pair if not (await self.confirm_oracle_conversion_rate()): self.notify("The strategy failed to start.") return if strategy_file_name: file_name = strategy_file_name.split(".")[0] self.strategy_file_name = file_name self.strategy_name = file_name elif not await self.status_check_all(notify_success=False): self.notify("Status checks failed. Start aborted.") return if self._last_started_strategy_file != self.strategy_file_name: init_logging("hummingbot_logs.yml", override_log_level=log_level.upper() if log_level else None, strategy_file_path=self.strategy_file_name) self._last_started_strategy_file = self.strategy_file_name # If macOS, disable App Nap. if platform.system() == "Darwin": import appnope appnope.nope() self._initialize_notifiers() try: self._initialize_strategy(self.strategy_name) except NotImplementedError: self.strategy_name = None self.strategy_file_name = None self.notify("Invalid strategy. Start aborted.") raise if any([str(exchange).endswith("paper_trade") for exchange in settings.required_exchanges]): self.notify("\nPaper Trading Active: All orders are simulated and no real orders are placed.") for exchange in settings.required_exchanges: connector: str = str(exchange) status: str = get_connector_status(connector) warning_msg: Optional[str] = warning_messages.get(connector, None) # confirm gateway connection conn_setting: settings.ConnectorSetting = settings.AllConnectorSettings.get_connector_settings()[connector] if conn_setting.uses_gateway_generic_connector(): connector_details: Dict[str, Any] = conn_setting.conn_init_parameters() if connector_details: data: List[List[str]] = [ ["chain", connector_details['chain']], ["network", connector_details['network']], ["wallet_address", connector_details['wallet_address']] ] await UserBalances.instance().update_exchange_balance(connector) balances: List[str] = [ f"{str(PerformanceMetrics.smart_round(v, 8))} {k}" for k, v in UserBalances.instance().all_balances(connector).items() ] data.append(["balances", ""]) for bal in balances: data.append(["", bal]) wallet_df: pd.DataFrame = pd.DataFrame(data=data, columns=["", f"{connector} configuration"]) self.notify(wallet_df.to_string(index=False)) self.app.clear_input() self.placeholder_mode = True use_configuration = await self.app.prompt(prompt="Do you want to continue? (Yes/No) >>> ") self.placeholder_mode = False self.app.change_prompt(prompt=">>> ") if use_configuration in ["N", "n", "No", "no"]: return if use_configuration not in ["Y", "y", "Yes", "yes"]: self.notify("Invalid input. Please execute the `start` command again.") return # Display custom warning message for specific connectors elif warning_msg is not None: self.notify(f"\nConnector status: {status}\n" f"{warning_msg}") # Display warning message if the exchange connector has outstanding issues or not working elif not status.endswith("GREEN"): self.notify(f"\nConnector status: {status}. This connector has one or more issues.\n" "Refer to our Github page for more info: https://github.com/coinalpha/hummingbot") self.notify(f"\nStatus check complete. Starting '{self.strategy_name}' strategy...") await self.start_market_making(restore) # We always start the RateOracle. It is required for PNL calculation. RateOracle.get_instance().start()
def test_get_rate_coingecko(self, mock_api): url = RateOracle.coingecko_usd_price_url.format( "cryptocurrency", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoCryptocurrencyPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "cryptocurrency", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoCryptocurrencyPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "decentralized-exchange", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoDEXPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "decentralized-exchange", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoDEXPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "decentralized-finance-defi", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoDEFIPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "decentralized-finance-defi", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoDEFIPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "smart-contract-platform", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoSmartContractPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "smart-contract-platform", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoSmartContractPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "stablecoins", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoStableCoinsPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "stablecoins", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoStableCoinsPage2), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "wrapped-tokens", 1, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoWrappedTokensPage1), repeat=True) url = RateOracle.coingecko_usd_price_url.format( "wrapped-tokens", 2, "USD") regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoWrappedTokensPage2), repeat=True) url = RateOracle.coingecko_supported_vs_tokens_url regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(Fixture.CoinGeckoVSCurrencies), repeat=True) rates = self.async_run_with_timeout( RateOracle.get_coingecko_prices_by_page("USD", 1, "cryptocurrency")) self._assert_rate_dict(rates) rates = self.async_run_with_timeout( RateOracle.get_coingecko_prices("USD")) self._assert_rate_dict(rates)
def profit_pct( self, rate_source: Optional[RateOracle] = None, account_for_fee: bool = False, ) -> Decimal: """ Returns a profit in percentage value (e.g. 0.01 for 1% profitability) Assumes the base token is the same in both arbitrage sides """ if not rate_source: rate_source = RateOracle.get_instance() buy_side: ArbProposalSide = self.first_side if self.first_side.is_buy else self.second_side sell_side: ArbProposalSide = self.first_side if not self.first_side.is_buy else self.second_side base_conversion_pair: str = f"{sell_side.market_info.base_asset}-{buy_side.market_info.base_asset}" quote_conversion_pair: str = f"{sell_side.market_info.quote_asset}-{buy_side.market_info.quote_asset}" sell_base_to_buy_base_rate: Decimal = Decimal(1) sell_quote_to_buy_quote_rate: Decimal = rate_source.rate(quote_conversion_pair) buy_fee_amount: Decimal = s_decimal_0 sell_fee_amount: Decimal = s_decimal_0 result: Decimal = s_decimal_0 if sell_quote_to_buy_quote_rate and sell_base_to_buy_base_rate: if account_for_fee: buy_trade_fee: TradeFeeBase = build_trade_fee( exchange=buy_side.market_info.market.name, is_maker=False, base_currency=buy_side.market_info.base_asset, quote_currency=buy_side.market_info.quote_asset, order_type=OrderType.MARKET, order_side=TradeType.BUY, amount=buy_side.amount, price=buy_side.order_price, extra_flat_fees=buy_side.extra_flat_fees ) sell_trade_fee: TradeFeeBase = build_trade_fee( exchange=sell_side.market_info.market.name, is_maker=False, base_currency=sell_side.market_info.base_asset, quote_currency=sell_side.market_info.quote_asset, order_type=OrderType.MARKET, order_side=TradeType.SELL, amount=sell_side.amount, price=sell_side.order_price, extra_flat_fees=sell_side.extra_flat_fees ) buy_fee_amount: Decimal = buy_trade_fee.fee_amount_in_token( trading_pair=buy_side.market_info.trading_pair, price=buy_side.quote_price, order_amount=buy_side.amount, token=buy_side.market_info.quote_asset, rate_source=rate_source ) sell_fee_amount: Decimal = sell_trade_fee.fee_amount_in_token( trading_pair=sell_side.market_info.trading_pair, price=sell_side.quote_price, order_amount=sell_side.amount, token=sell_side.market_info.quote_asset, rate_source=rate_source ) buy_spent_net: Decimal = (buy_side.amount * buy_side.quote_price) + buy_fee_amount sell_gained_net: Decimal = (sell_side.amount * sell_side.quote_price) - sell_fee_amount sell_gained_net_in_buy_quote_currency: Decimal = ( sell_gained_net * sell_quote_to_buy_quote_rate / sell_base_to_buy_base_rate ) result: Decimal = ( ((sell_gained_net_in_buy_quote_currency - buy_spent_net) / buy_spent_net) if buy_spent_net != s_decimal_0 else s_decimal_0 ) else: self.logger().warning("The arbitrage proposal profitability could not be calculated due to a missing rate" f" ({base_conversion_pair}={sell_base_to_buy_base_rate}," f" {quote_conversion_pair}={sell_quote_to_buy_quote_rate})") return result