diff --git a/README.md b/README.md index eb9e13553d..f668e3d96f 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ We created hummingbot to promote **decentralized market-making**: enabling membe | logo | id | name | ver | doc | status | |:---:|:---:|:---:|:---:|:---:|:---:| +| AscendEx | ascend_ex | [AscendEx](https://ascendex.com/en/global-digital-asset-platform) | 1 | [API](https://ascendex.github.io/ascendex-pro-api/#ascendex-pro-api-documentation) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | | Beaxy | beaxy | [Beaxy](https://beaxy.com/) | 2 | [API](https://beaxyapiv2trading.docs.apiary.io/) |![YELLOW](https://via.placeholder.com/15/ffff00/?text=+) | | Binance | binance | [Binance](https://www.binance.com/) | 3 | [API](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | | Binance US | binance_us | [Binance US](https://www.binance.com/) | 3 | [API](https://github.com/binance-us/binance-official-api-docs/blob/master/rest-api.md) |![YELLOW](https://via.placeholder.com/15/ffff00/?text=+) | | Binance Perpetual | binance_perpetual | [Binance Futures](https://www.binance.com/) | 1 | [API](https://binance-docs.github.io/apidocs/futures/en/) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | |Bittrex Global| bittrex | [Bittrex Global](https://global.bittrex.com/) | 3 | [API](https://bittrex.github.io/api/v3) |![YELLOW](https://via.placeholder.com/15/ffff00/?text=+) | | Bitfinex | bitfinex | [Bitfinex](https://www.bitfinex.com/) | 2 | [API](https://docs.bitfinex.com/docs/introduction) |![YELLOW](https://via.placeholder.com/15/ffff00/?text=+) | -| BitMax | bitmax | [BitMax](https://bitmax.io/en/global-digital-asset-platform) | 1 | [API](https://bitmax-exchange.github.io/bitmax-pro-api/#bitmax-pro-api-documentation) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | | Blocktane | blocktane | [Blocktane](https://blocktane.io/) | 2 | [API](https://blocktane.io/api) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | | Coinbase Pro | coinbase_pro | [Coinbase Pro](https://pro.coinbase.com/) | * | [API](https://docs.pro.coinbase.com/) |![GREEN](https://via.placeholder.com/15/008000/?text=+) | | Crypto.com | crypto_com | [Crypto.com](https://crypto.com/exchange) | 2 | [API](https://exchange-docs.crypto.com/#introduction) |![YELLOW](https://via.placeholder.com/15/ffff00/?text=+) | diff --git a/assets/ascend_ex_logo.png b/assets/ascend_ex_logo.png new file mode 100644 index 0000000000..c30f757cf4 Binary files /dev/null and b/assets/ascend_ex_logo.png differ diff --git a/assets/bitmax_logo.png b/assets/bitmax_logo.png deleted file mode 100644 index 362daeca21..0000000000 Binary files a/assets/bitmax_logo.png and /dev/null differ diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 2e145380a5..30e803ce39 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -45,12 +45,7 @@ "send_error_logs", "script_enabled", "script_file_path", - "manual_gas_price", "ethereum_chain_name", - "ethgasstation_gas_enabled", - "ethgasstation_api_key", - "ethgasstation_gas_level", - "ethgasstation_refresh_time", "gateway_enabled", "gateway_cert_passphrase", "gateway_api_host", @@ -240,9 +235,12 @@ async def inventory_price_prompt( exchange = config_map["exchange"].value market = config_map["market"].value base_asset, quote_asset = market.split("-") - balances = await UserBalances.instance().balances( - exchange, base_asset, quote_asset - ) + if global_config_map["paper_trade_enabled"].value: + balances = global_config_map["paper_trade_account_balance"].value + else: + balances = await UserBalances.instance().balances( + exchange, base_asset, quote_asset + ) if balances.get(base_asset) is None: return diff --git a/hummingbot/client/command/create_command.py b/hummingbot/client/command/create_command.py index eb6d6e7487..4ca413be94 100644 --- a/hummingbot/client/command/create_command.py +++ b/hummingbot/client/command/create_command.py @@ -97,6 +97,9 @@ async def prompt_a_config(self, # type: HummingbotApplication config: ConfigVar, input_value=None, assign_default=True): + if config.key == "inventory_price": + await self.inventory_price_prompt(self.strategy_config_map, input_value) + return if input_value is None: if assign_default: self.app.set_text(parse_config_default_to_text(config)) diff --git a/hummingbot/client/command/start_command.py b/hummingbot/client/command/start_command.py index bbb0690972..a8b516d9ec 100644 --- a/hummingbot/client/command/start_command.py +++ b/hummingbot/client/command/start_command.py @@ -22,7 +22,6 @@ from hummingbot.core.utils.kill_switch import KillSwitch from typing import TYPE_CHECKING from hummingbot.client.config.global_config_map import global_config_map -from hummingbot.core.utils.eth_gas_station_lookup import EthGasStationLookup from hummingbot.script.script_iterator import ScriptIterator from hummingbot.connector.connector_status import get_connector_status, warning_messages from hummingbot.client.config.config_var import ConfigVar @@ -146,9 +145,6 @@ async def start_market_making(self, # type: HummingbotApplication self.clock.add_iterator(self._script_iterator) self._notify(f"Script ({script_file}) started.") - if global_config_map["ethgasstation_gas_enabled"].value and settings.ethereum_gas_station_required(): - EthGasStationLookup.get_instance().start() - self.strategy_task: asyncio.Task = safe_ensure_future(self._run_clock(), loop=self.ev_loop) self._notify(f"\n'{strategy_name}' strategy started.\n" f"Run `status` command to query the progress.") diff --git a/hummingbot/client/command/status_command.py b/hummingbot/client/command/status_command.py index 3cf92785ad..1fb5e69344 100644 --- a/hummingbot/client/command/status_command.py +++ b/hummingbot/client/command/status_command.py @@ -18,7 +18,7 @@ ) from hummingbot.client.config.security import Security from hummingbot.user.user_balances import UserBalances -from hummingbot.client.settings import required_exchanges, ethereum_wallet_required, ethereum_gas_station_required +from hummingbot.client.settings import required_exchanges, ethereum_wallet_required from hummingbot.core.utils.async_utils import safe_ensure_future from typing import TYPE_CHECKING @@ -186,10 +186,6 @@ async def status_check_all(self, # type: HummingbotApplication else: self._notify(" - ETH wallet check: ETH wallet is not connected.") - if ethereum_gas_station_required() and not global_config_map["ethgasstation_gas_enabled"].value: - self._notify(f' - ETH gas station check: Manual gas price is fixed at ' - f'{global_config_map["manual_gas_price"].value}.') - loading_markets: List[ConnectorBase] = [] for market in self.markets.values(): if not market.ready: diff --git a/hummingbot/client/command/stop_command.py b/hummingbot/client/command/stop_command.py index bf5340f827..3413f2d90f 100644 --- a/hummingbot/client/command/stop_command.py +++ b/hummingbot/client/command/stop_command.py @@ -3,7 +3,6 @@ import threading from typing import TYPE_CHECKING from hummingbot.core.utils.async_utils import safe_ensure_future -from hummingbot.core.utils.eth_gas_station_lookup import EthGasStationLookup from hummingbot.core.rate_oracle.rate_oracle import RateOracle if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -49,9 +48,6 @@ async def stop_loop(self, # type: HummingbotApplication if RateOracle.get_instance().started: RateOracle.get_instance().stop() - if EthGasStationLookup.get_instance().started: - EthGasStationLookup.get_instance().stop() - if self.markets_recorder is not None: self.markets_recorder.stop() diff --git a/hummingbot/client/config/global_config_map.py b/hummingbot/client/config/global_config_map.py index 6ccf2eeda4..7a6c96fa23 100644 --- a/hummingbot/client/config/global_config_map.py +++ b/hummingbot/client/config/global_config_map.py @@ -298,32 +298,6 @@ def global_token_symbol_on_validated(value: str): type_str="decimal", validator=lambda v: validate_decimal(v, Decimal(0), inclusive=False), default=50), - "ethgasstation_gas_enabled": - ConfigVar(key="ethgasstation_gas_enabled", - prompt="Do you want to enable Ethereum gas station price lookup? >>> ", - required_if=lambda: False, - type_str="bool", - validator=validate_bool, - default=False), - "ethgasstation_api_key": - ConfigVar(key="ethgasstation_api_key", - prompt="Enter API key for defipulse.com gas station API >>> ", - required_if=lambda: global_config_map["ethgasstation_gas_enabled"].value, - type_str="str"), - "ethgasstation_gas_level": - ConfigVar(key="ethgasstation_gas_level", - prompt="Enter gas level you want to use for Ethereum transactions (fast, fastest, safeLow, average) " - ">>> ", - required_if=lambda: global_config_map["ethgasstation_gas_enabled"].value, - type_str="str", - validator=lambda s: None if s in {"fast", "fastest", "safeLow", "average"} - else "Invalid gas level."), - "ethgasstation_refresh_time": - ConfigVar(key="ethgasstation_refresh_time", - prompt="Enter refresh time for Ethereum gas price lookup (in seconds) >>> ", - required_if=lambda: global_config_map["ethgasstation_gas_enabled"].value, - type_str="int", - default=120), "gateway_api_host": ConfigVar(key="gateway_api_host", prompt=None, diff --git a/hummingbot/client/hummingbot_application.py b/hummingbot/client/hummingbot_application.py index 7d025e2d68..f777132f21 100644 --- a/hummingbot/client/hummingbot_application.py +++ b/hummingbot/client/hummingbot_application.py @@ -21,7 +21,6 @@ from hummingbot.client.errors import InvalidCommandError, ArgumentParserError from hummingbot.client.config.global_config_map import global_config_map, using_wallet from hummingbot.client.config.config_helpers import ( - get_erc20_token_addresses, get_strategy_config_map, get_connector_class, get_eth_wallet_private_key, @@ -194,10 +193,14 @@ def _initialize_market_assets(market_name: str, trading_pairs: List[str]) -> Lis return market_trading_pairs def _initialize_wallet(self, token_trading_pairs: List[str]): + # Todo: This function should be removed as it's currently not used by current working connectors + if not using_wallet(): return - if not self.token_list: - self.token_list = get_erc20_token_addresses() + # Commented this out for now since get_erc20_token_addresses uses blocking call + + # if not self.token_list: + # self.token_list = get_erc20_token_addresses() ethereum_wallet = global_config_map.get("ethereum_wallet").value private_key = Security._private_keys[ethereum_wallet] diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index e1bc1e131d..179c6b2e8f 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -7,7 +7,6 @@ import time import ssl import copy -import itertools as it from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL from hummingbot.core.utils import async_ttl_cache from hummingbot.core.network_iterator import NetworkStatus @@ -31,9 +30,9 @@ from hummingbot.connector.connector_base import ConnectorBase from hummingbot.connector.connector.balancer.balancer_in_flight_order import BalancerInFlightOrder from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH -from hummingbot.core.utils.eth_gas_station_lookup import get_gas_price from hummingbot.client.config.global_config_map import global_config_map -from hummingbot.client.config.config_helpers import get_erc20_token_addresses +from hummingbot.core.utils.ethereum import check_transaction_exceptions, fetch_trading_pairs +from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None s_decimal_0 = Decimal("0") @@ -71,12 +70,9 @@ def __init__(self, """ super().__init__() self._trading_pairs = trading_pairs - tokens = set() + self._tokens = set() for trading_pair in trading_pairs: - tokens.update(set(trading_pair.split("-"))) - self._erc_20_token_list = self.token_list() - self._token_addresses = {t: l[0] for t, l in self._erc_20_token_list.items() if t in tokens} - self._token_decimals = {t: l[1] for t, l in self._erc_20_token_list.items() if t in tokens} + self._tokens.update(set(trading_pair.split("-"))) self._wallet_private_key = wallet_private_key self._ethereum_rpc_url = ethereum_rpc_url self._trading_required = trading_required @@ -84,10 +80,13 @@ def __init__(self, self._shared_client = None self._last_poll_timestamp = 0.0 self._last_balance_poll_timestamp = time.time() + self._last_est_gas_cost_reported = 0 self._in_flight_orders = {} self._allowances = {} self._status_polling_task = None self._auto_approve_task = None + self._initiate_pool_task = None + self._initiate_pool_status = None self._real_time_balance_update = False self._max_swaps = global_config_map['balancer_max_swaps'].value self._poll_notifier = None @@ -96,17 +95,9 @@ def __init__(self, def name(self): return "balancer" - @staticmethod - def token_list(): - return get_erc20_token_addresses() - @staticmethod async def fetch_trading_pairs() -> List[str]: - token_list = BalancerConnector.token_list() - trading_pairs = [] - for base, quote in it.permutations(token_list.keys(), 2): - trading_pairs.append(f"{base}-{quote}") - return trading_pairs + return await fetch_trading_pairs() @property def limit_orders(self) -> List[LimitOrder]: @@ -115,6 +106,26 @@ def limit_orders(self) -> List[LimitOrder]: for in_flight_order in self._in_flight_orders.values() ] + async def initiate_pool(self) -> str: + """ + Initiate connector and cache pools + """ + try: + self.logger().info(f"Initializing Balancer connector and caching pools for {self._trading_pairs}.") + resp = await self._api_request("get", "eth/balancer/start", + {"pairs": json.dumps(self._trading_pairs)}) + status = bool(str(resp["success"])) + if bool(str(resp["success"])): + self._initiate_pool_status = status + except asyncio.CancelledError: + raise + except Exception as e: + self.logger().network( + f"Error initializing {self._trading_pairs} swap pools", + exc_info=True, + app_warning_msg=str(e) + ) + async def auto_approve(self): """ Automatically approves Balancer contract as a spender for token in trading pairs. @@ -138,9 +149,7 @@ async def approve_balancer_spender(self, token_symbol: str) -> Decimal: """ resp = await self._api_request("post", "eth/approve", - {"tokenAddress": self._token_addresses[token_symbol], - "gasPrice": str(get_gas_price()), - "decimals": self._token_decimals[token_symbol], # if not supplied, gateway would treat it eth-like with 18 decimals + {"token": token_symbol, "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -155,12 +164,11 @@ async def get_allowances(self) -> Dict[str, Decimal]: :return: A dictionary of token and its allowance (how much Balancer can spend). """ ret_val = {} - resp = await self._api_request("post", "eth/allowances-2", - {"tokenAddressList": ("".join([tok + "," for tok in self._token_addresses.values()])).rstrip(","), - "tokenDecimalList": ("".join([str(dec) + "," for dec in self._token_decimals.values()])).rstrip(","), + resp = await self._api_request("post", "eth/allowances", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]", "connector": self.name}) - for address, amount in resp["approvals"].items(): - ret_val[self.get_token(address)] = Decimal(str(amount)) + for token, amount in resp["approvals"].items(): + ret_val[token] = Decimal(str(amount)) return ret_val @async_ttl_cache(ttl=5, maxsize=10) @@ -177,15 +185,44 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal base, quote = trading_pair.split("-") side = "buy" if is_buy else "sell" resp = await self._api_request("post", - f"balancer/{side}-price", - {"base": self._token_addresses[base], - "quote": self._token_addresses[quote], + "eth/balancer/price", + {"base": base, + "quote": quote, "amount": amount, - "base_decimals": self._token_decimals[base], - "quote_decimals": self._token_decimals[quote], - "maxSwaps": self._max_swaps}) - if resp["price"] is not None: - return Decimal(str(resp["price"])) + "side": side.upper()}) + required_items = ["price", "gasLimit", "gasPrice", "gasCost"] + if any(item not in resp.keys() for item in required_items): + if "info" in resp.keys(): + self.logger().info(f"Unable to get price. {resp['info']}") + else: + self.logger().info(f"Missing data from price result. Incomplete return result for ({resp.keys()})") + else: + gas_limit = resp["gasLimit"] + gas_price = resp["gasPrice"] + gas_cost = resp["gasCost"] + price = resp["price"] + account_standing = { + "allowances": self._allowances, + "balances": self._account_balances, + "base": base, + "quote": quote, + "amount": amount, + "side": side, + "gas_limit": gas_limit, + "gas_price": gas_price, + "gas_cost": gas_cost, + "price": price, + "swaps": len(resp["swaps"]) + } + exceptions = check_transaction_exceptions(account_standing) + for index in range(len(exceptions)): + self.logger().info(f"Warning! [{index+1}/{len(exceptions)}] {side} order - {exceptions[index]}") + + if price is not None and len(exceptions) == 0: + # TODO standardize quote price object to include price, fee, token, is fee part of quote. + fee_overrides_config_map["balancer_maker_fee_amount"].value = Decimal(str(gas_cost)) + fee_overrides_config_map["balancer_taker_fee_amount"].value = Decimal(str(gas_cost)) + return Decimal(str(price)) except asyncio.CancelledError: raise except Exception as e: @@ -255,24 +292,28 @@ async def _create_order(self, amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) base, quote = trading_pair.split("-") - gas_price = get_gas_price() - api_params = {"base": self._token_addresses[base], - "quote": self._token_addresses[quote], + api_params = {"base": base, + "quote": quote, + "side": trade_type.name.upper(), "amount": str(amount), - "maxPrice": str(price), - "maxSwaps": str(self._max_swaps), - "gasPrice": str(gas_price), - "base_decimals": self._token_decimals[base], - "quote_decimals": self._token_decimals[quote], + "limitPrice": str(price), } - self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) try: - order_result = await self._api_request("post", f"balancer/{trade_type.name.lower()}", api_params) + order_result = await self._api_request("post", "eth/balancer/trade", api_params) hash = order_result.get("txHash") + gas_price = order_result.get("gasPrice") + gas_limit = order_result.get("gasLimit") + gas_cost = order_result.get("gasCost") + self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) tracked_order = self._in_flight_orders.get(order_id) + + # update onchain balance + await self._update_balances() + if tracked_order is not None: self.logger().info(f"Created {trade_type.name} order {order_id} txHash: {hash} " - f"for {amount} {trading_pair}.") + f"for {amount} {trading_pair}. Estimated Gas Cost: {gas_cost} ETH " + f" (gas limit: {gas_limit}, gas price: {gas_price})") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price if hash is not None: @@ -340,7 +381,7 @@ async def _update_order_status(self): for tracked_order in tracked_orders: order_id = await tracked_order.get_exchange_order_id() tasks.append(self._api_request("post", - "eth/get-receipt", + "eth/poll", {"txHash": order_id})) update_results = await safe_gather(*tasks, return_exceptions=True) for update_result in update_results: @@ -416,7 +457,7 @@ def has_allowances(self) -> bool: """ Checks if all tokens have allowance (an amount approved) """ - return len(self._allowances.values()) == len(self._token_addresses.values()) and \ + return len(self._allowances.values()) == len(self._tokens) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property @@ -429,6 +470,7 @@ def status_dict(self) -> Dict[str, bool]: async def start_network(self): if self._trading_required: self._status_polling_task = safe_ensure_future(self._status_polling_loop()) + self._initiate_pool_task = safe_ensure_future(self.initiate_pool()) self._auto_approve_task = safe_ensure_future(self.auto_approve()) async def stop_network(self): @@ -438,6 +480,9 @@ async def stop_network(self): if self._auto_approve_task is not None: self._auto_approve_task.cancel() self._auto_approve_task = None + if self._initiate_pool_task is not None: + self._initiate_pool_task.cancel() + self._initiate_pool_task = None async def check_network(self) -> NetworkStatus: try: @@ -478,9 +523,6 @@ async def _status_polling_loop(self): app_warning_msg="Could not fetch balances from Gateway API.") await asyncio.sleep(0.5) - def get_token(self, token_address: str) -> str: - return [k for k, v in self._token_addresses.items() if v == token_address][0] - async def _update_balances(self, on_interval = False): """ Calls Eth API to update total and available balances. @@ -492,12 +534,10 @@ async def _update_balances(self, on_interval = False): local_asset_names = set(self._account_balances.keys()) remote_asset_names = set() resp_json = await self._api_request("post", - "eth/balances-2", - {"tokenAddressList": ("".join([tok + "," for tok in self._token_addresses.values()])).rstrip(","), - "tokenDecimalList": ("".join([str(dec) + "," for dec in self._token_decimals.values()])).rstrip(",")}) + "eth/balances", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]"}) + for token, bal in resp_json["balances"].items(): - if len(token) > 4: - token = self.get_token(token) self._account_available_balances[token] = Decimal(str(bal)) self._account_balances[token] = Decimal(str(bal)) remote_asset_names.add(token) @@ -554,7 +594,7 @@ async def _api_request(self, err_msg = f" Message: {parsed_response['error']}" raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.{err_msg}") if "error" in parsed_response: - raise Exception(f"Error: {parsed_response['error']}") + raise Exception(f"Error: {parsed_response['error']} {parsed_response['message']}") return parsed_response diff --git a/hummingbot/connector/connector/terra/terra_connector.py b/hummingbot/connector/connector/terra/terra_connector.py index 1836750621..0f27b3e0ed 100644 --- a/hummingbot/connector/connector/terra/terra_connector.py +++ b/hummingbot/connector/connector/terra/terra_connector.py @@ -107,7 +107,7 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal base, quote = trading_pair.split("-") side = "buy" if is_buy else "sell" - resp = await self._api_request("post", "terra/price", {"base": base, "quote": quote, "trade_type": side, + resp = await self._api_request("post", "terra/price", {"base": base, "quote": quote, "side": side, "amount": str(amount)}) txFee = resp["txFee"] / float(amount) price_with_txfee = resp["price"] + txFee if is_buy else resp["price"] - txFee @@ -185,9 +185,9 @@ async def _create_order(self, base, quote = trading_pair.split("-") api_params = {"base": base, "quote": quote, - "trade_type": "buy" if trade_type is TradeType.BUY else "sell", + "side": "buy" if trade_type is TradeType.BUY else "sell", "amount": str(amount), - "secret": self._terra_wallet_seeds, + "privateKey": self._terra_wallet_seeds, # "maxPrice": str(price), } self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 2fef254945..c8277b65b1 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -7,7 +7,6 @@ import time import ssl import copy -import itertools as it from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL from hummingbot.core.utils import async_ttl_cache from hummingbot.core.network_iterator import NetworkStatus @@ -31,9 +30,9 @@ from hummingbot.connector.connector_base import ConnectorBase from hummingbot.connector.connector.uniswap.uniswap_in_flight_order import UniswapInFlightOrder from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH -from hummingbot.core.utils.eth_gas_station_lookup import get_gas_price from hummingbot.client.config.global_config_map import global_config_map -from hummingbot.client.config.config_helpers import get_erc20_token_addresses +from hummingbot.core.utils.ethereum import check_transaction_exceptions, fetch_trading_pairs +from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None s_decimal_0 = Decimal("0") @@ -71,12 +70,9 @@ def __init__(self, """ super().__init__() self._trading_pairs = trading_pairs - tokens = set() + self._tokens = set() for trading_pair in trading_pairs: - tokens.update(set(trading_pair.split("-"))) - self._erc_20_token_list = self.token_list() - self._token_addresses = {t: l[0] for t, l in self._erc_20_token_list.items() if t in tokens} - self._token_decimals = {t: l[1] for t, l in self._erc_20_token_list.items() if t in tokens} + self._tokens.update(set(trading_pair.split("-"))) self._wallet_private_key = wallet_private_key self._ethereum_rpc_url = ethereum_rpc_url self._trading_required = trading_required @@ -84,10 +80,13 @@ def __init__(self, self._shared_client = None self._last_poll_timestamp = 0.0 self._last_balance_poll_timestamp = time.time() + self._last_est_gas_cost_reported = 0 self._in_flight_orders = {} self._allowances = {} self._status_polling_task = None self._auto_approve_task = None + self._initiate_pool_task = None + self._initiate_pool_status = None self._real_time_balance_update = False self._poll_notifier = None @@ -95,17 +94,9 @@ def __init__(self, def name(self): return "uniswap" - @staticmethod - def token_list(): - return get_erc20_token_addresses() - @staticmethod async def fetch_trading_pairs() -> List[str]: - token_list = UniswapConnector.token_list() - trading_pairs = [] - for base, quote in it.permutations(token_list.keys(), 2): - trading_pairs.append(f"{base}-{quote}") - return trading_pairs + return await fetch_trading_pairs() @property def limit_orders(self) -> List[LimitOrder]: @@ -114,6 +105,26 @@ def limit_orders(self) -> List[LimitOrder]: for in_flight_order in self._in_flight_orders.values() ] + async def initiate_pool(self) -> str: + """ + Initiate connector and start caching paths for trading_pairs + """ + try: + self.logger().info(f"Initializing Uniswap connector and paths for {self._trading_pairs} pairs.") + resp = await self._api_request("get", "eth/uniswap/start", + {"pairs": json.dumps(self._trading_pairs)}) + status = bool(str(resp["success"])) + if bool(str(resp["success"])): + self._initiate_pool_status = status + except asyncio.CancelledError: + raise + except Exception as e: + self.logger().network( + f"Error initializing {self._trading_pairs} ", + exc_info=True, + app_warning_msg=str(e) + ) + async def auto_approve(self): """ Automatically approves Uniswap contract as a spender for token in trading pairs. @@ -137,9 +148,7 @@ async def approve_uniswap_spender(self, token_symbol: str) -> Decimal: """ resp = await self._api_request("post", "eth/approve", - {"tokenAddress": self._token_addresses[token_symbol], - "gasPrice": str(get_gas_price()), - "decimals": self._token_decimals[token_symbol], # if not supplied, gateway would treat it eth-like with 18 decimals + {"token": token_symbol, "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -154,12 +163,11 @@ async def get_allowances(self) -> Dict[str, Decimal]: :return: A dictionary of token and its allowance (how much Uniswap can spend). """ ret_val = {} - resp = await self._api_request("post", "eth/allowances-2", - {"tokenAddressList": ("".join([tok + "," for tok in self._token_addresses.values()])).rstrip(","), - "tokenDecimalList": ("".join([str(dec) + "," for dec in self._token_decimals.values()])).rstrip(","), + resp = await self._api_request("post", "eth/allowances", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]", "connector": self.name}) - for address, amount in resp["approvals"].items(): - ret_val[self.get_token(address)] = Decimal(str(amount)) + for token, amount in resp["approvals"].items(): + ret_val[token] = Decimal(str(amount)) return ret_val @async_ttl_cache(ttl=5, maxsize=10) @@ -176,12 +184,43 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal base, quote = trading_pair.split("-") side = "buy" if is_buy else "sell" resp = await self._api_request("post", - f"uniswap/{side}-price", - {"base": self._token_addresses[base], - "quote": self._token_addresses[quote], + "eth/uniswap/price", + {"base": base, + "quote": quote, + "side": side.upper(), "amount": amount}) - if resp["price"] is not None: - return Decimal(str(resp["price"])) + required_items = ["price", "gasLimit", "gasPrice", "gasCost"] + if any(item not in resp.keys() for item in required_items): + if "info" in resp.keys(): + self.logger().info(f"Unable to get price. {resp['info']}") + else: + self.logger().info(f"Missing data from price result. Incomplete return result for ({resp.keys()})") + else: + gas_limit = resp["gasLimit"] + gas_price = resp["gasPrice"] + gas_cost = resp["gasCost"] + price = resp["price"] + account_standing = { + "allowances": self._allowances, + "balances": self._account_balances, + "base": base, + "quote": quote, + "amount": amount, + "side": side, + "gas_limit": gas_limit, + "gas_price": gas_price, + "gas_cost": gas_cost, + "price": price + } + exceptions = check_transaction_exceptions(account_standing) + for index in range(len(exceptions)): + self.logger().info(f"Warning! [{index+1}/{len(exceptions)}] {side} order - {exceptions[index]}") + + if price is not None and len(exceptions) == 0: + # TODO standardize quote price object to include price, fee, token, is fee part of quote. + fee_overrides_config_map["uniswap_maker_fee_amount"].value = Decimal(str(gas_cost)) + fee_overrides_config_map["uniswap_taker_fee_amount"].value = Decimal(str(gas_cost)) + return Decimal(str(price)) except asyncio.CancelledError: raise except Exception as e: @@ -251,21 +290,24 @@ async def _create_order(self, amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) base, quote = trading_pair.split("-") - gas_price = get_gas_price() - api_params = {"base": self._token_addresses[base], - "quote": self._token_addresses[quote], + api_params = {"base": base, + "quote": quote, + "side": trade_type.name.upper(), "amount": str(amount), - "maxPrice": str(price), - "gasPrice": str(gas_price), + "limitPrice": str(price), } - self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) try: - order_result = await self._api_request("post", f"uniswap/{trade_type.name.lower()}", api_params) + order_result = await self._api_request("post", "eth/uniswap/trade", api_params) hash = order_result.get("txHash") + gas_price = order_result.get("gasPrice") + gas_limit = order_result.get("gasLimit") + gas_cost = order_result.get("gasCost") + self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info(f"Created {trade_type.name} order {order_id} txHash: {hash} " - f"for {amount} {trading_pair}.") + f"for {amount} {trading_pair}. Estimated Gas Cost: {gas_cost} ETH " + f" (gas limit: {gas_limit}, gas price: {gas_price})") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price if hash is not None: @@ -333,7 +375,7 @@ async def _update_order_status(self): for tracked_order in tracked_orders: order_id = await tracked_order.get_exchange_order_id() tasks.append(self._api_request("post", - "eth/get-receipt", + "eth/poll", {"txHash": order_id})) update_results = await safe_gather(*tasks, return_exceptions=True) for update_result in update_results: @@ -409,7 +451,7 @@ def has_allowances(self) -> bool: """ Checks if all tokens have allowance (an amount approved) """ - return len(self._allowances.values()) == len(self._token_addresses.values()) and \ + return len(self._allowances.values()) == len(self._tokens) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property @@ -422,6 +464,7 @@ def status_dict(self) -> Dict[str, bool]: async def start_network(self): if self._trading_required: self._status_polling_task = safe_ensure_future(self._status_polling_loop()) + self._initiate_pool_task = safe_ensure_future(self.initiate_pool()) self._auto_approve_task = safe_ensure_future(self.auto_approve()) async def stop_network(self): @@ -431,6 +474,9 @@ async def stop_network(self): if self._auto_approve_task is not None: self._auto_approve_task.cancel() self._auto_approve_task = None + if self._initiate_pool_task is not None: + self._initiate_pool_task.cancel() + self._initiate_pool_task = None async def check_network(self) -> NetworkStatus: try: @@ -458,7 +504,7 @@ async def _status_polling_loop(self): self._poll_notifier = asyncio.Event() await self._poll_notifier.wait() await safe_gather( - self._update_balances(), + self._update_balances(on_interval=True), self._update_order_status(), ) self._last_poll_timestamp = self.current_timestamp @@ -471,37 +517,32 @@ async def _status_polling_loop(self): app_warning_msg="Could not fetch balances from Gateway API.") await asyncio.sleep(0.5) - def get_token(self, token_address: str) -> str: - return [k for k, v in self._token_addresses.items() if v == token_address][0] - - async def _update_balances(self): + async def _update_balances(self, on_interval = False): """ Calls Eth API to update total and available balances. """ last_tick = self._last_balance_poll_timestamp current_tick = self.current_timestamp - if (current_tick - last_tick) > self.UPDATE_BALANCE_INTERVAL: + if not on_interval or (current_tick - last_tick) > self.UPDATE_BALANCE_INTERVAL: self._last_balance_poll_timestamp = current_tick - local_asset_names = set(self._account_balances.keys()) - remote_asset_names = set() - resp_json = await self._api_request("post", - "eth/balances-2", - {"tokenAddressList": ("".join([tok + "," for tok in self._token_addresses.values()])).rstrip(","), - "tokenDecimalList": ("".join([str(dec) + "," for dec in self._token_decimals.values()])).rstrip(",")}) - for token, bal in resp_json["balances"].items(): - if len(token) > 4: - token = self.get_token(token) - self._account_available_balances[token] = Decimal(str(bal)) - self._account_balances[token] = Decimal(str(bal)) - remote_asset_names.add(token) - - asset_names_to_remove = local_asset_names.difference(remote_asset_names) - for asset_name in asset_names_to_remove: - del self._account_available_balances[asset_name] - del self._account_balances[asset_name] - - self._in_flight_orders_snapshot = {k: copy.copy(v) for k, v in self._in_flight_orders.items()} - self._in_flight_orders_snapshot_timestamp = self.current_timestamp + local_asset_names = set(self._account_balances.keys()) + remote_asset_names = set() + resp_json = await self._api_request("post", + "eth/balances", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]"}) + + for token, bal in resp_json["balances"].items(): + self._account_available_balances[token] = Decimal(str(bal)) + self._account_balances[token] = Decimal(str(bal)) + remote_asset_names.add(token) + + asset_names_to_remove = local_asset_names.difference(remote_asset_names) + for asset_name in asset_names_to_remove: + del self._account_available_balances[asset_name] + del self._account_balances[asset_name] + + self._in_flight_orders_snapshot = {k: copy.copy(v) for k, v in self._in_flight_orders.items()} + self._in_flight_orders_snapshot_timestamp = self.current_timestamp async def _http_client(self) -> aiohttp.ClientSession: """ @@ -547,7 +588,7 @@ async def _api_request(self, err_msg = f" Message: {parsed_response['error']}" raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.{err_msg}") if "error" in parsed_response: - raise Exception(f"Error: {parsed_response['error']}") + raise Exception(f"Error: {parsed_response['error']} {parsed_response['message']}") return parsed_response diff --git a/hummingbot/connector/connector_status.py b/hummingbot/connector/connector_status.py index 2f35999bb7..14a0968094 100644 --- a/hummingbot/connector/connector_status.py +++ b/hummingbot/connector/connector_status.py @@ -8,7 +8,7 @@ 'binance_perpetual_testnet': 'green', 'binance_us': 'yellow', 'bitfinex': 'yellow', - 'bitmax': 'green', + 'ascend_ex': 'green', 'bittrex': 'yellow', 'blocktane': 'green', 'celo': 'green', diff --git a/hummingbot/connector/exchange/bitmax/__init__.py b/hummingbot/connector/exchange/ascend_ex/__init__.py similarity index 100% rename from hummingbot/connector/exchange/bitmax/__init__.py rename to hummingbot/connector/exchange/ascend_ex/__init__.py diff --git a/hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pxd b/hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pxd similarity index 90% rename from hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pxd rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pxd index fbc1eb3080..833d9864a0 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pxd +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pxd @@ -1,7 +1,7 @@ # distutils: language=c++ cimport numpy as np -cdef class BitmaxActiveOrderTracker: +cdef class AscendExActiveOrderTracker: cdef dict _active_bids cdef dict _active_asks diff --git a/hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pyx b/hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pyx similarity index 93% rename from hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pyx rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pyx index 092a97c45c..b80930b8b2 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_active_order_tracker.pyx +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_active_order_tracker.pyx @@ -12,12 +12,12 @@ from hummingbot.core.data_type.order_book_row import OrderBookRow _logger = None s_empty_diff = np.ndarray(shape=(0, 4), dtype="float64") -BitmaxOrderBookTrackingDictionary = Dict[Decimal, Dict[str, Dict[str, any]]] +AscendExOrderBookTrackingDictionary = Dict[Decimal, Dict[str, Dict[str, any]]] -cdef class BitmaxActiveOrderTracker: +cdef class AscendExActiveOrderTracker: def __init__(self, - active_asks: BitmaxOrderBookTrackingDictionary = None, - active_bids: BitmaxOrderBookTrackingDictionary = None): + active_asks: AscendExOrderBookTrackingDictionary = None, + active_bids: AscendExOrderBookTrackingDictionary = None): super().__init__() self._active_asks = active_asks or {} self._active_bids = active_bids or {} @@ -30,11 +30,11 @@ cdef class BitmaxActiveOrderTracker: return _logger @property - def active_asks(self) -> BitmaxOrderBookTrackingDictionary: + def active_asks(self) -> AscendExOrderBookTrackingDictionary: return self._active_asks @property - def active_bids(self) -> BitmaxOrderBookTrackingDictionary: + def active_bids(self) -> AscendExOrderBookTrackingDictionary: return self._active_bids # TODO: research this more diff --git a/hummingbot/connector/exchange/bitmax/bitmax_api_order_book_data_source.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_order_book_data_source.py similarity index 91% rename from hummingbot/connector/exchange/bitmax/bitmax_api_order_book_data_source.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_api_order_book_data_source.py index 306b0631c4..0c64a9c1c4 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_order_book_data_source.py @@ -13,13 +13,13 @@ from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource from hummingbot.core.utils.async_utils import safe_gather from hummingbot.logger import HummingbotLogger -from hummingbot.connector.exchange.bitmax.bitmax_active_order_tracker import BitmaxActiveOrderTracker -from hummingbot.connector.exchange.bitmax.bitmax_order_book import BitmaxOrderBook -from hummingbot.connector.exchange.bitmax.bitmax_utils import convert_from_exchange_trading_pair, convert_to_exchange_trading_pair -from hummingbot.connector.exchange.bitmax.bitmax_constants import EXCHANGE_NAME, REST_URL, WS_URL, PONG_PAYLOAD +from hummingbot.connector.exchange.ascend_ex.ascend_ex_active_order_tracker import AscendExActiveOrderTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book import AscendExOrderBook +from hummingbot.connector.exchange.ascend_ex.ascend_ex_utils import convert_from_exchange_trading_pair, convert_to_exchange_trading_pair +from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import EXCHANGE_NAME, REST_URL, WS_URL, PONG_PAYLOAD -class BitmaxAPIOrderBookDataSource(OrderBookTrackerDataSource): +class AscendExAPIOrderBookDataSource(OrderBookTrackerDataSource): MAX_RETRIES = 20 MESSAGE_TIMEOUT = 30.0 SNAPSHOT_TIMEOUT = 10.0 @@ -105,13 +105,13 @@ async def get_order_book_data(trading_pair: str) -> Dict[str, any]: async def get_new_order_book(self, trading_pair: str) -> OrderBook: snapshot: Dict[str, Any] = await self.get_order_book_data(trading_pair) snapshot_timestamp: float = snapshot.get("data").get("ts") - snapshot_msg: OrderBookMessage = BitmaxOrderBook.snapshot_message_from_exchange( + snapshot_msg: OrderBookMessage = AscendExOrderBook.snapshot_message_from_exchange( snapshot.get("data"), snapshot_timestamp, metadata={"trading_pair": trading_pair} ) order_book = self.order_book_create_function() - active_order_tracker: BitmaxActiveOrderTracker = BitmaxActiveOrderTracker() + active_order_tracker: AscendExActiveOrderTracker = AscendExActiveOrderTracker() bids, asks = active_order_tracker.convert_snapshot_message_to_order_book_row(snapshot_msg) order_book.apply_snapshot(bids, asks, snapshot_msg.update_id) return order_book @@ -141,7 +141,7 @@ async def listen_for_trades(self, ev_loop: asyncio.BaseEventLoop, output: asynci for trade in msg.get("data"): trade_timestamp: int = trade.get("ts") - trade_msg: OrderBookMessage = BitmaxOrderBook.trade_message_from_exchange( + trade_msg: OrderBookMessage = AscendExOrderBook.trade_message_from_exchange( trade, trade_timestamp, metadata={"trading_pair": trading_pair} @@ -181,7 +181,7 @@ async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, outp msg_timestamp: int = msg.get("data").get("ts") trading_pair: str = convert_from_exchange_trading_pair(msg.get("symbol")) - order_book_message: OrderBookMessage = BitmaxOrderBook.diff_message_from_exchange( + order_book_message: OrderBookMessage = AscendExOrderBook.diff_message_from_exchange( msg.get("data"), msg_timestamp, metadata={"trading_pair": trading_pair} @@ -207,7 +207,7 @@ async def listen_for_order_book_snapshots(self, ev_loop: asyncio.BaseEventLoop, try: snapshot: Dict[str, any] = await self.get_order_book_data(trading_pair) snapshot_timestamp: float = snapshot.get("data").get("ts") - snapshot_msg: OrderBookMessage = BitmaxOrderBook.snapshot_message_from_exchange( + snapshot_msg: OrderBookMessage = AscendExOrderBook.snapshot_message_from_exchange( snapshot.get("data"), snapshot_timestamp, metadata={"trading_pair": trading_pair} diff --git a/hummingbot/connector/exchange/bitmax/bitmax_api_user_stream_data_source.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py similarity index 79% rename from hummingbot/connector/exchange/bitmax/bitmax_api_user_stream_data_source.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py index 10bbac6763..19571d90c8 100755 --- a/hummingbot/connector/exchange/bitmax/bitmax_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py @@ -9,11 +9,12 @@ from typing import Optional, List, AsyncIterable, Any from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.logger import HummingbotLogger -from hummingbot.connector.exchange.bitmax.bitmax_auth import BitmaxAuth -from hummingbot.connector.exchange.bitmax.bitmax_constants import REST_URL, getWsUrlPriv, PONG_PAYLOAD +from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth +from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import REST_URL, PONG_PAYLOAD +from hummingbot.connector.exchange.ascend_ex.ascend_ex_utils import get_ws_url_private -class BitmaxAPIUserStreamDataSource(UserStreamTrackerDataSource): +class AscendExAPIUserStreamDataSource(UserStreamTrackerDataSource): MAX_RETRIES = 20 MESSAGE_TIMEOUT = 10.0 PING_TIMEOUT = 5.0 @@ -26,8 +27,8 @@ def logger(cls) -> HummingbotLogger: cls._logger = logging.getLogger(__name__) return cls._logger - def __init__(self, bitmax_auth: BitmaxAuth, trading_pairs: Optional[List[str]] = []): - self._bitmax_auth: BitmaxAuth = bitmax_auth + def __init__(self, ascend_ex_auth: AscendExAuth, trading_pairs: Optional[List[str]] = []): + self._ascend_ex_auth: AscendExAuth = ascend_ex_auth self._trading_pairs = trading_pairs self._current_listen_key = None self._listen_for_user_stream_task = None @@ -50,18 +51,18 @@ async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: a while True: try: response = await aiohttp.ClientSession().get(f"{REST_URL}/info", headers={ - **self._bitmax_auth.get_headers(), - **self._bitmax_auth.get_auth_headers("info"), + **self._ascend_ex_auth.get_headers(), + **self._ascend_ex_auth.get_auth_headers("info"), }) info = await response.json() accountGroup = info.get("data").get("accountGroup") - headers = self._bitmax_auth.get_auth_headers("stream") + headers = self._ascend_ex_auth.get_auth_headers("stream") payload = { "op": "sub", "ch": "order:cash" } - async with websockets.connect(f"{getWsUrlPriv(accountGroup)}/stream", extra_headers=headers) as ws: + async with websockets.connect(f"{get_ws_url_private(accountGroup)}/stream", extra_headers=headers) as ws: try: ws: websockets.WebSocketClientProtocol = ws await ws.send(ujson.dumps(payload)) @@ -75,12 +76,12 @@ async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: a output.put_nowait(msg) except Exception: self.logger().error( - "Unexpected error when parsing Bitmax message. ", exc_info=True + "Unexpected error when parsing AscendEx message. ", exc_info=True ) raise except Exception: self.logger().error( - "Unexpected error while listening to Bitmax messages. ", exc_info=True + "Unexpected error while listening to AscendEx messages. ", exc_info=True ) raise finally: @@ -89,7 +90,7 @@ async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: a raise except Exception: self.logger().error( - "Unexpected error with Bitmax WebSocket connection. " "Retrying after 30 seconds...", exc_info=True + "Unexpected error with AscendEx WebSocket connection. " "Retrying after 30 seconds...", exc_info=True ) await asyncio.sleep(30.0) diff --git a/hummingbot/connector/exchange/bitmax/bitmax_auth.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_auth.py similarity index 79% rename from hummingbot/connector/exchange/bitmax/bitmax_auth.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_auth.py index 646fb13a8a..23b2c78f67 100755 --- a/hummingbot/connector/exchange/bitmax/bitmax_auth.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_auth.py @@ -1,13 +1,13 @@ import hmac import hashlib from typing import Dict, Any -from hummingbot.connector.exchange.bitmax.bitmax_utils import get_ms_timestamp +from hummingbot.connector.exchange.ascend_ex.ascend_ex_utils import get_ms_timestamp -class BitmaxAuth(): +class AscendExAuth(): """ - Auth class required by bitmax API - Learn more at https://bitmax-exchange.github.io/bitmax-pro-api/#authenticate-a-restful-request + Auth class required by AscendEx API + Learn more at https://ascendex.github.io/ascendex-pro-api/#authenticate-a-restful-request """ def __init__(self, api_key: str, secret_key: str): self.api_key = api_key @@ -39,7 +39,7 @@ def get_auth_headers( def get_headers(self) -> Dict[str, Any]: """ - Generates generic headers required by bitmax + Generates generic headers required by AscendEx :return: a dictionary of headers """ diff --git a/hummingbot/connector/exchange/ascend_ex/ascend_ex_constants.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_constants.py new file mode 100644 index 0000000000..27165b3257 --- /dev/null +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_constants.py @@ -0,0 +1,7 @@ +# A single source of truth for constant variables related to the exchange + + +EXCHANGE_NAME = "ascend_ex" +REST_URL = "https://ascendex.com/api/pro/v1" +WS_URL = "wss://ascendex.com/0/api/pro/v1/stream" +PONG_PAYLOAD = {"op": "pong"} diff --git a/hummingbot/connector/exchange/bitmax/bitmax_exchange.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_exchange.py similarity index 88% rename from hummingbot/connector/exchange/bitmax/bitmax_exchange.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_exchange.py index bef04d3a52..a8aac4e618 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_exchange.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_exchange.py @@ -36,25 +36,25 @@ TradeFee ) from hummingbot.connector.exchange_base import ExchangeBase -from hummingbot.connector.exchange.bitmax.bitmax_order_book_tracker import BitmaxOrderBookTracker -from hummingbot.connector.exchange.bitmax.bitmax_user_stream_tracker import BitmaxUserStreamTracker -from hummingbot.connector.exchange.bitmax.bitmax_auth import BitmaxAuth -from hummingbot.connector.exchange.bitmax.bitmax_in_flight_order import BitmaxInFlightOrder -from hummingbot.connector.exchange.bitmax import bitmax_utils -from hummingbot.connector.exchange.bitmax.bitmax_constants import EXCHANGE_NAME, REST_URL, getRestUrlPriv +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book_tracker import AscendExOrderBookTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_user_stream_tracker import AscendExUserStreamTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth +from hummingbot.connector.exchange.ascend_ex.ascend_ex_in_flight_order import AscendExInFlightOrder +from hummingbot.connector.exchange.ascend_ex import ascend_ex_utils +from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import EXCHANGE_NAME, REST_URL from hummingbot.core.data_type.common import OpenOrder ctce_logger = None s_decimal_NaN = Decimal("nan") -BitmaxTradingRule = namedtuple("BitmaxTradingRule", "minNotional maxNotional") -BitmaxOrder = namedtuple("BitmaxOrder", "symbol price orderQty orderType avgPx cumFee cumFilledQty errorCode feeAsset lastExecTime orderId seqNum side status stopPrice execInst") -BitmaxBalance = namedtuple("BitmaxBalance", "asset availableBalance totalBalance") +AscendExTradingRule = namedtuple("AscendExTradingRule", "minNotional maxNotional") +AscendExOrder = namedtuple("AscendExOrder", "symbol price orderQty orderType avgPx cumFee cumFilledQty errorCode feeAsset lastExecTime orderId seqNum side status stopPrice execInst") +AscendExBalance = namedtuple("AscendExBalance", "asset availableBalance totalBalance") -class BitmaxExchange(ExchangeBase): +class AscendExExchange(ExchangeBase): """ - BitmaxExchange connects with Bitmax exchange and provides order book pricing, user account tracking and + AscendExExchange connects with AscendEx exchange and provides order book pricing, user account tracking and trading functionality. """ API_CALL_TIMEOUT = 10.0 @@ -70,31 +70,31 @@ def logger(cls) -> HummingbotLogger: return ctce_logger def __init__(self, - bitmax_api_key: str, - bitmax_secret_key: str, + ascend_ex_api_key: str, + ascend_ex_secret_key: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True ): """ - :param bitmax_api_key: The API key to connect to private Bitmax APIs. - :param bitmax_secret_key: The API secret. + :param ascend_ex_api_key: The API key to connect to private AscendEx APIs. + :param ascend_ex_secret_key: The API secret. :param trading_pairs: The market trading pairs which to track order book data. :param trading_required: Whether actual trading is needed. """ super().__init__() self._trading_required = trading_required self._trading_pairs = trading_pairs - self._bitmax_auth = BitmaxAuth(bitmax_api_key, bitmax_secret_key) - self._order_book_tracker = BitmaxOrderBookTracker(trading_pairs=trading_pairs) - self._user_stream_tracker = BitmaxUserStreamTracker(self._bitmax_auth, trading_pairs) + self._ascend_ex_auth = AscendExAuth(ascend_ex_api_key, ascend_ex_secret_key) + self._order_book_tracker = AscendExOrderBookTracker(trading_pairs=trading_pairs) + self._user_stream_tracker = AscendExUserStreamTracker(self._ascend_ex_auth, trading_pairs) self._ev_loop = asyncio.get_event_loop() self._shared_client = None self._poll_notifier = asyncio.Event() self._last_timestamp = 0 - self._in_flight_orders = {} # Dict[client_order_id:str, BitmaxInFlightOrder] + self._in_flight_orders = {} # Dict[client_order_id:str, AscendExInFlightOrder] self._order_not_found_records = {} # Dict[client_order_id:str, count:int] self._trading_rules = {} # Dict[trading_pair:str, TradingRule] - self._bitmax_trading_rules = {} # Dict[trading_pair:str, BitmaxTradingRule] + self._ascend_ex_trading_rules = {} # Dict[trading_pair:str, AscendExTradingRule] self._status_polling_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None @@ -115,7 +115,7 @@ def trading_rules(self) -> Dict[str, TradingRule]: return self._trading_rules @property - def in_flight_orders(self) -> Dict[str, BitmaxInFlightOrder]: + def in_flight_orders(self) -> Dict[str, AscendExInFlightOrder]: return self._in_flight_orders @property @@ -126,7 +126,7 @@ def status_dict(self) -> Dict[str, bool]: return { "order_books_initialized": self._order_book_tracker.ready, "account_balance": len(self._account_balances) > 0 if self._trading_required else True, - "trading_rule_initialized": len(self._trading_rules) > 0 and len(self._bitmax_trading_rules) > 0, + "trading_rule_initialized": len(self._trading_rules) > 0 and len(self._ascend_ex_trading_rules) > 0, "user_stream_initialized": self._user_stream_tracker.data_source.last_recv_time > 0 if self._trading_required else True, "account_data": self._account_group is not None and self._account_uid is not None @@ -165,7 +165,7 @@ def restore_tracking_states(self, saved_states: Dict[str, any]): :param saved_states: The saved tracking_states. """ self._in_flight_orders.update({ - key: BitmaxInFlightOrder.from_json(value) + key: AscendExInFlightOrder.from_json(value) for key, value in saved_states.items() }) @@ -258,22 +258,22 @@ async def _trading_rules_polling_loop(self): except Exception as e: self.logger().network(f"Unexpected error while fetching trading rules. Error: {str(e)}", exc_info=True, - app_warning_msg="Could not fetch new trading rules from Bitmax. " + app_warning_msg="Could not fetch new trading rules from AscendEx. " "Check network connection.") await asyncio.sleep(0.5) async def _update_trading_rules(self): instruments_info = await self._api_request("get", path_url="products") - [trading_rules, bitmax_trading_rules] = self._format_trading_rules(instruments_info) + [trading_rules, ascend_ex_trading_rules] = self._format_trading_rules(instruments_info) self._trading_rules.clear() self._trading_rules = trading_rules - self._bitmax_trading_rules.clear() - self._bitmax_trading_rules = bitmax_trading_rules + self._ascend_ex_trading_rules.clear() + self._ascend_ex_trading_rules = ascend_ex_trading_rules def _format_trading_rules( self, instruments_info: Dict[str, Any] - ) -> [Dict[str, TradingRule], Dict[str, Dict[str, BitmaxTradingRule]]]: + ) -> [Dict[str, TradingRule], Dict[str, Dict[str, AscendExTradingRule]]]: """ Converts json API response into a dictionary of trading rules. :param instruments_info: The json API response @@ -299,27 +299,27 @@ def _format_trading_rules( } """ trading_rules = {} - bitmax_trading_rules = {} + ascend_ex_trading_rules = {} for rule in instruments_info["data"]: try: - trading_pair = bitmax_utils.convert_from_exchange_trading_pair(rule["symbol"]) + trading_pair = ascend_ex_utils.convert_from_exchange_trading_pair(rule["symbol"]) trading_rules[trading_pair] = TradingRule( trading_pair, min_price_increment=Decimal(rule["tickSize"]), min_base_amount_increment=Decimal(rule["lotSize"]) ) - bitmax_trading_rules[trading_pair] = BitmaxTradingRule( + ascend_ex_trading_rules[trading_pair] = AscendExTradingRule( minNotional=Decimal(rule["minNotional"]), maxNotional=Decimal(rule["maxNotional"]) ) except Exception: self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True) - return [trading_rules, bitmax_trading_rules] + return [trading_rules, ascend_ex_trading_rules] async def _update_account_data(self): headers = { - **self._bitmax_auth.get_headers(), - **self._bitmax_auth.get_auth_headers("info"), + **self._ascend_ex_auth.get_headers(), + **self._ascend_ex_auth.get_auth_headers("info"), } url = f"{REST_URL}/info" response = await aiohttp.ClientSession().get(url, headers=headers) @@ -359,16 +359,16 @@ async def _api_request(self, if (self._account_group) is None: await self._update_account_data() - url = f"{getRestUrlPriv(self._account_group)}/{path_url}" + url = f"{ascend_ex_utils.get_rest_url_private(self._account_group)}/{path_url}" headers = { - **self._bitmax_auth.get_headers(), - **self._bitmax_auth.get_auth_headers( + **self._ascend_ex_auth.get_headers(), + **self._ascend_ex_auth.get_auth_headers( path_url if force_auth_path_url is None else force_auth_path_url ), } else: url = f"{REST_URL}/{path_url}" - headers = self._bitmax_auth.get_headers() + headers = self._ascend_ex_auth.get_headers() client = await self._http_client() if method == "get": @@ -433,7 +433,7 @@ def buy(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET, :param price: The price (note: this is no longer optional) :returns A new internal order id """ - client_order_id = bitmax_utils.gen_client_order_id(True, trading_pair) + client_order_id = ascend_ex_utils.gen_client_order_id(True, trading_pair) safe_ensure_future(self._create_order(TradeType.BUY, client_order_id, trading_pair, amount, order_type, price)) return client_order_id @@ -448,7 +448,7 @@ def sell(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET, :param price: The price (note: this is no longer optional) :returns A new internal order id """ - client_order_id = bitmax_utils.gen_client_order_id(False, trading_pair) + client_order_id = ascend_ex_utils.gen_client_order_id(False, trading_pair) safe_ensure_future(self._create_order(TradeType.SELL, client_order_id, trading_pair, amount, order_type, price)) return client_order_id @@ -480,25 +480,25 @@ async def _create_order(self, """ if not order_type.is_limit_type(): raise Exception(f"Unsupported order type: {order_type}") - bitmax_trading_rule = self._bitmax_trading_rules[trading_pair] + ascend_ex_trading_rule = self._ascend_ex_trading_rules[trading_pair] amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) try: - # bitmax has a unique way of determening if the order has enough "worth" to be posted - # see https://bitmax-exchange.github.io/bitmax-pro-api/#place-order + # ascend_ex has a unique way of determening if the order has enough "worth" to be posted + # see https://ascendex.github.io/ascendex-pro-api/#place-order notional = Decimal(price * amount) - if notional < bitmax_trading_rule.minNotional or notional > bitmax_trading_rule.maxNotional: - raise ValueError(f"Notional amount {notional} is not withing the range of {bitmax_trading_rule.minNotional}-{bitmax_trading_rule.maxNotional}.") + if notional < ascend_ex_trading_rule.minNotional or notional > ascend_ex_trading_rule.maxNotional: + raise ValueError(f"Notional amount {notional} is not withing the range of {ascend_ex_trading_rule.minNotional}-{ascend_ex_trading_rule.maxNotional}.") # TODO: check balance - [exchange_order_id, timestamp] = bitmax_utils.gen_exchange_order_id(self._account_uid, order_id) + [exchange_order_id, timestamp] = ascend_ex_utils.gen_exchange_order_id(self._account_uid, order_id) api_params = { "id": exchange_order_id, "time": timestamp, - "symbol": bitmax_utils.convert_to_exchange_trading_pair(trading_pair), + "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(trading_pair), "orderPrice": f"{price:f}", "orderQty": f"{amount:f}", "orderType": "limit", @@ -517,7 +517,6 @@ async def _create_order(self, await self._api_request("post", "cash/order", api_params, True, force_auth_path_url="order") tracked_order = self._in_flight_orders.get(order_id) - # tracked_order.update_exchange_order_id(exchange_order_id) if tracked_order is not None: self.logger().info(f"Created {order_type.name} {trade_type.name} order {order_id} for " @@ -540,7 +539,7 @@ async def _create_order(self, except Exception as e: self.stop_tracking_order(order_id) self.logger().network( - f"Error submitting {trade_type.name} {order_type.name} order to Bitmax for " + f"Error submitting {trade_type.name} {order_type.name} order to AscendEx for " f"{amount} {trading_pair} " f"{price}.", exc_info=True, @@ -560,7 +559,7 @@ def start_tracking_order(self, """ Starts tracking an order by simply adding it into _in_flight_orders dictionary. """ - self._in_flight_orders[order_id] = BitmaxInFlightOrder( + self._in_flight_orders[order_id] = AscendExInFlightOrder( client_order_id=order_id, exchange_order_id=exchange_order_id, trading_pair=trading_pair, @@ -596,9 +595,9 @@ async def _execute_cancel(self, trading_pair: str, order_id: str) -> str: "delete", "cash/order", { - "symbol": bitmax_utils.convert_to_exchange_trading_pair(trading_pair), + "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(trading_pair), "orderId": ex_order_id, - "time": bitmax_utils.get_ms_timestamp() + "time": ascend_ex_utils.get_ms_timestamp() }, True, force_auth_path_url="order" @@ -615,7 +614,7 @@ async def _execute_cancel(self, trading_pair: str, order_id: str) -> str: self.logger().network( f"Failed to cancel order {order_id}: {str(e)}", exc_info=True, - app_warning_msg=f"Failed to cancel the order {order_id} on Bitmax. " + app_warning_msg=f"Failed to cancel the order {order_id} on AscendEx. " f"Check API key and network connection." ) @@ -639,7 +638,7 @@ async def _status_polling_loop(self): self.logger().error(str(e), exc_info=True) self.logger().network("Unexpected error while fetching account updates.", exc_info=True, - app_warning_msg="Could not fetch account updates from Bitmax. " + app_warning_msg="Could not fetch account updates from AscendEx. " "Check API key and network connection.") await asyncio.sleep(0.5) @@ -649,7 +648,7 @@ async def _update_balances(self): """ response = await self._api_request("get", "cash/balance", {}, True, force_auth_path_url="balance") balances = list(map( - lambda balance: BitmaxBalance( + lambda balance: AscendExBalance( balance["asset"], balance["availableBalance"], balance["totalBalance"] @@ -687,7 +686,7 @@ async def _update_order_status(self): continue order_data = response.get("data") - self._process_order_message(BitmaxOrder( + self._process_order_message(AscendExOrder( order_data["symbol"], order_data["price"], order_data["orderQty"], @@ -738,7 +737,7 @@ async def cancel_all(self, timeout_seconds: float): self.logger().network( "Failed to cancel all orders.", exc_info=True, - app_warning_msg="Failed to cancel all orders on Bitmax. Check API key and network connection." + app_warning_msg="Failed to cancel all orders on AscendEx. Check API key and network connection." ) return cancellation_results @@ -783,14 +782,14 @@ async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]: self.logger().network( "Unknown error. Retrying after 1 seconds.", exc_info=True, - app_warning_msg="Could not fetch user events from Bitmax. Check API key and network connection." + app_warning_msg="Could not fetch user events from AscendEx. Check API key and network connection." ) await asyncio.sleep(1.0) async def _user_stream_event_listener(self): """ Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by - BitmaxAPIUserStreamDataSource. + AscendExAPIUserStreamDataSource. """ async for event_message in self._iter_user_event_queue(): try: @@ -798,7 +797,7 @@ async def _user_stream_event_listener(self): order_data = event_message.get("data") trading_pair = order_data["s"] base_asset, quote_asset = tuple(asset for asset in trading_pair.split("/")) - self._process_order_message(BitmaxOrder( + self._process_order_message(AscendExOrder( trading_pair, order_data["p"], order_data["q"], @@ -817,12 +816,12 @@ async def _user_stream_event_listener(self): order_data["ei"] )) # Handles balance updates from orders. - base_asset_balance = BitmaxBalance( + base_asset_balance = AscendExBalance( base_asset, order_data["bab"], order_data["btb"] ) - quote_asset_balance = BitmaxBalance( + quote_asset_balance = AscendExBalance( quote_asset, order_data["qab"], order_data["qtb"] @@ -831,7 +830,7 @@ async def _user_stream_event_listener(self): elif event_message.get("m") == "balance": # Handles balance updates from Deposits/Withdrawals, Transfers between Cash and Margin Accounts balance_data = event_message.get("data") - balance = BitmaxBalance( + balance = AscendExBalance( balance_data["a"], balance_data["ab"], balance_data["tb"] @@ -869,7 +868,7 @@ async def get_open_orders(self) -> List[OpenOrder]: ret_val.append( OpenOrder( client_order_id=client_order_id, - trading_pair=bitmax_utils.convert_from_exchange_trading_pair(order["symbol"]), + trading_pair=ascend_ex_utils.convert_from_exchange_trading_pair(order["symbol"]), price=Decimal(str(order["price"])), amount=Decimal(str(order["orderQty"])), executed_amount=Decimal(str(order["cumFilledQty"])), @@ -882,7 +881,7 @@ async def get_open_orders(self) -> List[OpenOrder]: ) return ret_val - def _process_order_message(self, order_msg: BitmaxOrder): + def _process_order_message(self, order_msg: AscendExOrder): """ Updates in-flight order and triggers cancellation or failure event if needed. :param order_msg: The order response from either REST or web socket API (they are of the same format) @@ -917,7 +916,7 @@ def _process_order_message(self, order_msg: BitmaxOrder): self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info(f"The market order {client_order_id} has failed according to order status API. " - f"Reason: {bitmax_utils.get_api_reason(order_msg.errorCode)}") + f"Reason: {ascend_ex_utils.get_api_reason(order_msg.errorCode)}") self.trigger_event(MarketEvent.OrderFailure, MarketOrderFailureEvent( self.current_timestamp, @@ -965,7 +964,7 @@ def _process_order_message(self, order_msg: BitmaxOrder): ) self.stop_tracking_order(client_order_id) - def _process_balances(self, balances: List[BitmaxBalance]): + def _process_balances(self, balances: List[AscendExBalance]): local_asset_names = set(self._account_balances.keys()) remote_asset_names = set() diff --git a/hummingbot/connector/exchange/bitmax/bitmax_in_flight_order.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_in_flight_order.py similarity index 95% rename from hummingbot/connector/exchange/bitmax/bitmax_in_flight_order.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_in_flight_order.py index b455433ba0..0654f2cba9 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_in_flight_order.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_in_flight_order.py @@ -12,7 +12,7 @@ from hummingbot.connector.in_flight_order_base import InFlightOrderBase -class BitmaxInFlightOrder(InFlightOrderBase): +class AscendExInFlightOrder(InFlightOrderBase): def __init__(self, client_order_id: str, exchange_order_id: Optional[str], @@ -53,7 +53,7 @@ def from_json(cls, data: Dict[str, Any]) -> InFlightOrderBase: :param data: json data from API :return: formatted InFlightOrder """ - retval = BitmaxInFlightOrder( + retval = AscendExInFlightOrder( data["client_order_id"], data["exchange_order_id"], data["trading_pair"], diff --git a/hummingbot/connector/exchange/bitmax/bitmax_order_book.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py similarity index 83% rename from hummingbot/connector/exchange/bitmax/bitmax_order_book.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py index be6702853d..88ebe8b0d1 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_order_book.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py @@ -1,24 +1,25 @@ #!/usr/bin/env python import logging -import hummingbot.connector.exchange.bitmax.bitmax_constants as constants +import hummingbot.connector.exchange.ascend_ex.ascend_ex_constants as constants from sqlalchemy.engine import RowProxy from typing import ( Optional, Dict, List, Any) -from hummingbot.logger import HummingbotLogger + from hummingbot.core.data_type.order_book import OrderBook from hummingbot.core.data_type.order_book_message import ( OrderBookMessage, OrderBookMessageType ) -from hummingbot.connector.exchange.bitmax.bitmax_order_book_message import BitmaxOrderBookMessage +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book_message import AscendExOrderBookMessage +from hummingbot.logger import HummingbotLogger _logger = None -class BitmaxOrderBook(OrderBook): +class AscendExOrderBook(OrderBook): @classmethod def logger(cls) -> HummingbotLogger: global _logger @@ -35,13 +36,13 @@ def snapshot_message_from_exchange(cls, Convert json snapshot data into standard OrderBookMessage format :param msg: json snapshot data from live web socket stream :param timestamp: timestamp attached to incoming data - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ if metadata: msg.update(metadata) - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.SNAPSHOT, content=msg, timestamp=timestamp @@ -53,9 +54,9 @@ def snapshot_message_from_db(cls, record: RowProxy, metadata: Optional[Dict] = N *used for backtesting Convert a row of snapshot data into standard OrderBookMessage format :param record: a row of snapshot data from the database - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.SNAPSHOT, content=record.json, timestamp=record.timestamp @@ -70,13 +71,13 @@ def diff_message_from_exchange(cls, Convert json diff data into standard OrderBookMessage format :param msg: json diff data from live web socket stream :param timestamp: timestamp attached to incoming data - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ if metadata: msg.update(metadata) - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.DIFF, content=msg, timestamp=timestamp @@ -88,9 +89,9 @@ def diff_message_from_db(cls, record: RowProxy, metadata: Optional[Dict] = None) *used for backtesting Convert a row of diff data into standard OrderBookMessage format :param record: a row of diff data from the database - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.DIFF, content=record.json, timestamp=record.timestamp @@ -104,7 +105,7 @@ def trade_message_from_exchange(cls, """ Convert a trade data into standard OrderBookMessage format :param record: a trade data from the database - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ if metadata: @@ -117,7 +118,7 @@ def trade_message_from_exchange(cls, "amount": msg.get("q"), }) - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.TRADE, content=msg, timestamp=timestamp @@ -129,9 +130,9 @@ def trade_message_from_db(cls, record: RowProxy, metadata: Optional[Dict] = None *used for backtesting Convert a row of trade data into standard OrderBookMessage format :param record: a row of trade data from the database - :return: BitmaxOrderBookMessage + :return: AscendExOrderBookMessage """ - return BitmaxOrderBookMessage( + return AscendExOrderBookMessage( message_type=OrderBookMessageType.TRADE, content=record.json, timestamp=record.timestamp @@ -142,5 +143,5 @@ def from_snapshot(cls, snapshot: OrderBookMessage): raise NotImplementedError(constants.EXCHANGE_NAME + " order book needs to retain individual order data.") @classmethod - def restore_from_snapshot_and_diffs(self, snapshot: OrderBookMessage, diffs: List[OrderBookMessage]): + def restore_from_snapshot_and_diffs(cls, snapshot: OrderBookMessage, diffs: List[OrderBookMessage]): raise NotImplementedError(constants.EXCHANGE_NAME + " order book needs to retain individual order data.") diff --git a/hummingbot/connector/exchange/bitmax/bitmax_order_book_message.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_message.py similarity index 95% rename from hummingbot/connector/exchange/bitmax/bitmax_order_book_message.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_message.py index a00550b66b..c5b0ef2e13 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_order_book_message.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_message.py @@ -13,7 +13,7 @@ ) -class BitmaxOrderBookMessage(OrderBookMessage): +class AscendExOrderBookMessage(OrderBookMessage): def __new__( cls, message_type: OrderBookMessageType, @@ -27,7 +27,7 @@ def __new__( raise ValueError("timestamp must not be None when initializing snapshot messages.") timestamp = content["timestamp"] - return super(BitmaxOrderBookMessage, cls).__new__( + return super(AscendExOrderBookMessage, cls).__new__( cls, message_type, content, timestamp=timestamp, *args, **kwargs ) diff --git a/hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py similarity index 75% rename from hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py index 9fa26cc508..0801dd47be 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py @@ -2,20 +2,23 @@ import asyncio import bisect import logging -import hummingbot.connector.exchange.bitmax.bitmax_constants as constants import time + +import hummingbot.connector.exchange.ascend_ex.ascend_ex_constants as constants + from collections import defaultdict, deque from typing import Optional, Dict, List, Deque + from hummingbot.core.data_type.order_book_message import OrderBookMessageType -from hummingbot.logger import HummingbotLogger from hummingbot.core.data_type.order_book_tracker import OrderBookTracker -from hummingbot.connector.exchange.bitmax.bitmax_order_book_message import BitmaxOrderBookMessage -from hummingbot.connector.exchange.bitmax.bitmax_active_order_tracker import BitmaxActiveOrderTracker -from hummingbot.connector.exchange.bitmax.bitmax_api_order_book_data_source import BitmaxAPIOrderBookDataSource -from hummingbot.connector.exchange.bitmax.bitmax_order_book import BitmaxOrderBook +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book_message import AscendExOrderBookMessage +from hummingbot.connector.exchange.ascend_ex.ascend_ex_active_order_tracker import AscendExActiveOrderTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source import AscendExAPIOrderBookDataSource +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book import AscendExOrderBook +from hummingbot.logger import HummingbotLogger -class BitmaxOrderBookTracker(OrderBookTracker): +class AscendExOrderBookTracker(OrderBookTracker): _logger: Optional[HummingbotLogger] = None @classmethod @@ -25,7 +28,7 @@ def logger(cls) -> HummingbotLogger: return cls._logger def __init__(self, trading_pairs: Optional[List[str]] = None,): - super().__init__(BitmaxAPIOrderBookDataSource(trading_pairs), trading_pairs) + super().__init__(AscendExAPIOrderBookDataSource(trading_pairs), trading_pairs) self._ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() self._order_book_snapshot_stream: asyncio.Queue = asyncio.Queue() @@ -33,10 +36,10 @@ def __init__(self, trading_pairs: Optional[List[str]] = None,): self._order_book_trade_stream: asyncio.Queue = asyncio.Queue() self._process_msg_deque_task: Optional[asyncio.Task] = None self._past_diffs_windows: Dict[str, Deque] = {} - self._order_books: Dict[str, BitmaxOrderBook] = {} - self._saved_message_queues: Dict[str, Deque[BitmaxOrderBookMessage]] = \ + self._order_books: Dict[str, AscendExOrderBook] = {} + self._saved_message_queues: Dict[str, Deque[AscendExOrderBookMessage]] = \ defaultdict(lambda: deque(maxlen=1000)) - self._active_order_trackers: Dict[str, BitmaxActiveOrderTracker] = defaultdict(BitmaxActiveOrderTracker) + self._active_order_trackers: Dict[str, AscendExActiveOrderTracker] = defaultdict(AscendExActiveOrderTracker) self._order_book_stream_listener_task: Optional[asyncio.Task] = None self._order_book_trade_listener_task: Optional[asyncio.Task] = None @@ -51,20 +54,20 @@ async def _track_single_book(self, trading_pair: str): """ Update an order book with changes from the latest batch of received messages """ - past_diffs_window: Deque[BitmaxOrderBookMessage] = deque() + past_diffs_window: Deque[AscendExOrderBookMessage] = deque() self._past_diffs_windows[trading_pair] = past_diffs_window message_queue: asyncio.Queue = self._tracking_message_queues[trading_pair] - order_book: BitmaxOrderBook = self._order_books[trading_pair] - active_order_tracker: BitmaxActiveOrderTracker = self._active_order_trackers[trading_pair] + order_book: AscendExOrderBook = self._order_books[trading_pair] + active_order_tracker: AscendExActiveOrderTracker = self._active_order_trackers[trading_pair] last_message_timestamp: float = time.time() diff_messages_accepted: int = 0 while True: try: - message: BitmaxOrderBookMessage = None - saved_messages: Deque[BitmaxOrderBookMessage] = self._saved_message_queues[trading_pair] + message: AscendExOrderBookMessage = None + saved_messages: Deque[AscendExOrderBookMessage] = self._saved_message_queues[trading_pair] # Process saved messages first if there are any if len(saved_messages) > 0: message = saved_messages.popleft() @@ -87,7 +90,7 @@ async def _track_single_book(self, trading_pair: str): diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: - past_diffs: List[BitmaxOrderBookMessage] = list(past_diffs_window) + past_diffs: List[AscendExOrderBookMessage] = list(past_diffs_window) # only replay diffs later than snapshot, first update active order with snapshot then replay diffs replay_position = bisect.bisect_right(past_diffs, message) replay_diffs = past_diffs[replay_position:] diff --git a/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker_entry.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker_entry.py new file mode 100644 index 0000000000..ebefecfebb --- /dev/null +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker_entry.py @@ -0,0 +1,21 @@ +from hummingbot.core.data_type.order_book import OrderBook +from hummingbot.core.data_type.order_book_tracker_entry import OrderBookTrackerEntry +from hummingbot.connector.exchange.ascend_ex.ascend_ex_active_order_tracker import AscendExActiveOrderTracker + + +class AscendExOrderBookTrackerEntry(OrderBookTrackerEntry): + def __init__( + self, trading_pair: str, timestamp: float, order_book: OrderBook, active_order_tracker: AscendExActiveOrderTracker + ): + self._active_order_tracker = active_order_tracker + super(AscendExOrderBookTrackerEntry, self).__init__(trading_pair, timestamp, order_book) + + def __repr__(self) -> str: + return ( + f"AscendExOrderBookTrackerEntry(trading_pair='{self._trading_pair}', timestamp='{self._timestamp}', " + f"order_book='{self._order_book}')" + ) + + @property + def active_order_tracker(self) -> AscendExActiveOrderTracker: + return self._active_order_tracker diff --git a/hummingbot/connector/exchange/bitmax/bitmax_user_stream_tracker.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_user_stream_tracker.py similarity index 69% rename from hummingbot/connector/exchange/bitmax/bitmax_user_stream_tracker.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_user_stream_tracker.py index 8255abdb94..8557ca44db 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_user_stream_tracker.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_user_stream_tracker.py @@ -2,12 +2,16 @@ import asyncio import logging + from typing import ( Optional, List, ) +from hummingbot.connector.exchange.ascend_ex.ascend_ex_api_user_stream_data_source import \ + AscendExAPIUserStreamDataSource +from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth +from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import EXCHANGE_NAME from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource -from hummingbot.logger import HummingbotLogger from hummingbot.core.data_type.user_stream_tracker import ( UserStreamTracker ) @@ -15,26 +19,24 @@ safe_ensure_future, safe_gather, ) -from hummingbot.connector.exchange.bitmax.bitmax_api_user_stream_data_source import \ - BitmaxAPIUserStreamDataSource -from hummingbot.connector.exchange.bitmax.bitmax_auth import BitmaxAuth -from hummingbot.connector.exchange.bitmax.bitmax_constants import EXCHANGE_NAME + +from hummingbot.logger import HummingbotLogger -class BitmaxUserStreamTracker(UserStreamTracker): - _cbpust_logger: Optional[HummingbotLogger] = None +class AscendExUserStreamTracker(UserStreamTracker): + _logger: Optional[HummingbotLogger] = None @classmethod def logger(cls) -> HummingbotLogger: - if cls._bust_logger is None: - cls._bust_logger = logging.getLogger(__name__) - return cls._bust_logger + if cls._logger is None: + cls._logger = logging.getLogger(__name__) + return cls._logger def __init__(self, - bitmax_auth: Optional[BitmaxAuth] = None, + ascend_ex_auth: Optional[AscendExAuth] = None, trading_pairs: Optional[List[str]] = []): super().__init__() - self._bitmax_auth: BitmaxAuth = bitmax_auth + self._ascend_ex_auth: AscendExAuth = ascend_ex_auth self._trading_pairs: List[str] = trading_pairs self._ev_loop: asyncio.events.AbstractEventLoop = asyncio.get_event_loop() self._data_source: Optional[UserStreamTrackerDataSource] = None @@ -48,8 +50,8 @@ def data_source(self) -> UserStreamTrackerDataSource: :return: OrderBookTrackerDataSource """ if not self._data_source: - self._data_source = BitmaxAPIUserStreamDataSource( - bitmax_auth=self._bitmax_auth, + self._data_source = AscendExAPIUserStreamDataSource( + ascend_ex_auth=self._ascend_ex_auth, trading_pairs=self._trading_pairs ) return self._data_source diff --git a/hummingbot/connector/exchange/bitmax/bitmax_utils.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_utils.py similarity index 77% rename from hummingbot/connector/exchange/bitmax/bitmax_utils.py rename to hummingbot/connector/exchange/ascend_ex/ascend_ex_utils.py index 93d33eb9d1..a8ac64400f 100644 --- a/hummingbot/connector/exchange/bitmax/bitmax_utils.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_utils.py @@ -18,6 +18,14 @@ HBOT_BROKER_ID = "HMBot" +def get_rest_url_private(account_id: int) -> str: + return f"https://ascendex.com/{account_id}/api/pro/v1" + + +def get_ws_url_private(account_id: int) -> str: + return f"wss://ascendex.com/{account_id}/api/pro/v1" + + def convert_from_exchange_trading_pair(exchange_trading_pair: str) -> str: return exchange_trading_pair.replace("/", "-") @@ -70,16 +78,16 @@ def gen_client_order_id(is_buy: bool, trading_pair: str) -> str: KEYS = { - "bitmax_api_key": - ConfigVar(key="bitmax_api_key", - prompt="Enter your Bitmax API key >>> ", - required_if=using_exchange("bitmax"), + "ascend_ex_api_key": + ConfigVar(key="ascend_ex_api_key", + prompt="Enter your AscendEx API key >>> ", + required_if=using_exchange("ascend_ex"), is_secure=True, is_connect_key=True), - "bitmax_secret_key": - ConfigVar(key="bitmax_secret_key", - prompt="Enter your Bitmax secret key >>> ", - required_if=using_exchange("bitmax"), + "ascend_ex_secret_key": + ConfigVar(key="ascend_ex_secret_key", + prompt="Enter your AscendEx secret key >>> ", + required_if=using_exchange("ascend_ex"), is_secure=True, is_connect_key=True), } diff --git a/hummingbot/connector/exchange/bitmax/bitmax_constants.py b/hummingbot/connector/exchange/bitmax/bitmax_constants.py deleted file mode 100644 index 51ec061543..0000000000 --- a/hummingbot/connector/exchange/bitmax/bitmax_constants.py +++ /dev/null @@ -1,15 +0,0 @@ -# A single source of truth for constant variables related to the exchange - - -EXCHANGE_NAME = "bitmax" -REST_URL = "https://bitmax.io/api/pro/v1" -WS_URL = "wss://bitmax.io/1/api/pro/v1/stream" -PONG_PAYLOAD = {"op": "pong"} - - -def getRestUrlPriv(accountId: int) -> str: - return f"https://bitmax.io/{accountId}/api/pro/v1" - - -def getWsUrlPriv(accountId: int) -> str: - return f"wss://bitmax.io/{accountId}/api/pro/v1" diff --git a/hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker_entry.py b/hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker_entry.py deleted file mode 100644 index a97a33088a..0000000000 --- a/hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker_entry.py +++ /dev/null @@ -1,21 +0,0 @@ -from hummingbot.core.data_type.order_book import OrderBook -from hummingbot.core.data_type.order_book_tracker_entry import OrderBookTrackerEntry -from hummingbot.connector.exchange.bitmax.bitmax_active_order_tracker import BitmaxActiveOrderTracker - - -class BitmaxOrderBookTrackerEntry(OrderBookTrackerEntry): - def __init__( - self, trading_pair: str, timestamp: float, order_book: OrderBook, active_order_tracker: BitmaxActiveOrderTracker - ): - self._active_order_tracker = active_order_tracker - super(BitmaxOrderBookTrackerEntry, self).__init__(trading_pair, timestamp, order_book) - - def __repr__(self) -> str: - return ( - f"BitmaxOrderBookTrackerEntry(trading_pair='{self._trading_pair}', timestamp='{self._timestamp}', " - f"order_book='{self._order_book}')" - ) - - @property - def active_order_tracker(self) -> BitmaxActiveOrderTracker: - return self._active_order_tracker diff --git a/hummingbot/core/utils/estimate_fee.py b/hummingbot/core/utils/estimate_fee.py index 78823e425e..5a52568861 100644 --- a/hummingbot/core/utils/estimate_fee.py +++ b/hummingbot/core/utils/estimate_fee.py @@ -2,16 +2,11 @@ from hummingbot.core.event.events import TradeFee, TradeFeeType from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map from hummingbot.client.settings import CONNECTOR_SETTINGS -from hummingbot.core.utils.eth_gas_station_lookup import get_gas_price, get_gas_limit def estimate_fee(exchange: str, is_maker: bool) -> TradeFee: if exchange not in CONNECTOR_SETTINGS: raise Exception(f"Invalid connector. {exchange} does not exist in CONNECTOR_SETTINGS") - use_gas = CONNECTOR_SETTINGS[exchange].use_eth_gas_lookup - if use_gas: - gas_amount = get_gas_price(in_gwei=False) * get_gas_limit(exchange) - return TradeFee(percent=0, flat_fees=[("ETH", gas_amount)]) fee_type = CONNECTOR_SETTINGS[exchange].fee_type fee_token = CONNECTOR_SETTINGS[exchange].fee_token default_fees = CONNECTOR_SETTINGS[exchange].default_fees diff --git a/hummingbot/core/utils/eth_gas_station_lookup.py b/hummingbot/core/utils/eth_gas_station_lookup.py deleted file mode 100644 index ee56502631..0000000000 --- a/hummingbot/core/utils/eth_gas_station_lookup.py +++ /dev/null @@ -1,178 +0,0 @@ -import asyncio -import requests -import logging -from typing import ( - Optional, - Dict, - Any -) -import aiohttp -from enum import Enum -from decimal import Decimal -from hummingbot.core.network_base import ( - NetworkBase, - NetworkStatus -) -from hummingbot.core.utils.async_utils import safe_ensure_future -from hummingbot.logger import HummingbotLogger -from hummingbot.client.config.global_config_map import global_config_map -from hummingbot.client.settings import CONNECTOR_SETTINGS -from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH - -ETH_GASSTATION_API_URL = "https://data-api.defipulse.com/api/v1/egs/api/ethgasAPI.json?api-key={}" - - -def get_gas_price(in_gwei: bool = True) -> Decimal: - if not global_config_map["ethgasstation_gas_enabled"].value: - gas_price = global_config_map["manual_gas_price"].value - else: - gas_price = EthGasStationLookup.get_instance().gas_price - return gas_price if in_gwei else gas_price / Decimal("1e9") - - -def get_gas_limit(connector_name: str) -> int: - gas_limit = request_gas_limit(connector_name) - return gas_limit - - -def request_gas_limit(connector_name: str) -> int: - host = global_config_map["gateway_api_host"].value - port = global_config_map["gateway_api_port"].value - balancer_max_swaps = global_config_map["balancer_max_swaps"].value - - base_url = ':'.join(['https://' + host, port]) - url = f"{base_url}/{connector_name}/gas-limit" - - ca_certs = GATEAWAY_CA_CERT_PATH - client_certs = (GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH) - params = {"maxSwaps": balancer_max_swaps} if connector_name == "balancer" else {} - response = requests.post(url, data=params, verify=ca_certs, cert=client_certs) - parsed_response = response.json() - if response.status_code != 200: - err_msg = "" - if "error" in parsed_response: - err_msg = f" Message: {parsed_response['error']}" - raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.{err_msg}") - if "error" in parsed_response: - raise Exception(f"Error: {parsed_response['error']}") - return parsed_response['gasLimit'] - - -class GasLevel(Enum): - fast = "fast" - fastest = "fastest" - safeLow = "safeLow" - average = "average" - - -class EthGasStationLookup(NetworkBase): - _egsl_logger: Optional[HummingbotLogger] = None - _shared_instance: "EthGasStationLookup" = None - - @classmethod - def get_instance(cls) -> "EthGasStationLookup": - if cls._shared_instance is None: - cls._shared_instance = EthGasStationLookup() - return cls._shared_instance - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._egsl_logger is None: - cls._egsl_logger = logging.getLogger(__name__) - return cls._egsl_logger - - def __init__(self): - super().__init__() - self._gas_prices: Dict[str, Decimal] = {} - self._gas_limits: Dict[str, Decimal] = {} - self._balancer_max_swaps: int = global_config_map["balancer_max_swaps"].value - self._async_task = None - - @property - def api_key(self): - return global_config_map["ethgasstation_api_key"].value - - @property - def gas_level(self) -> GasLevel: - return GasLevel[global_config_map["ethgasstation_gas_level"].value] - - @property - def refresh_time(self): - return global_config_map["ethgasstation_refresh_time"].value - - @property - def gas_price(self): - return self._gas_prices[self.gas_level] - - @property - def gas_limits(self): - return self._gas_limits - - @gas_limits.setter - def gas_limits(self, gas_limits: Dict[str, int]): - for key, value in gas_limits.items(): - self._gas_limits[key] = value - - @property - def balancer_max_swaps(self): - return self._balancer_max_swaps - - @balancer_max_swaps.setter - def balancer_max_swaps(self, max_swaps: int): - self._balancer_max_swaps = max_swaps - - async def gas_price_update_loop(self): - while True: - try: - url = ETH_GASSTATION_API_URL.format(self.api_key) - async with aiohttp.ClientSession() as client: - response = await client.get(url=url) - if response.status != 200: - raise IOError(f"Error fetching current gas prices. " - f"HTTP status is {response.status}.") - resp_data: Dict[str, Any] = await response.json() - for key, value in resp_data.items(): - if key in GasLevel.__members__: - self._gas_prices[GasLevel[key]] = Decimal(str(value)) / Decimal("10") - prices_str = ', '.join([k.name + ': ' + str(v) for k, v in self._gas_prices.items()]) - self.logger().info(f"Gas levels: [{prices_str}]") - for name, con_setting in CONNECTOR_SETTINGS.items(): - if con_setting.use_eth_gas_lookup: - self._gas_limits[name] = get_gas_limit(name) - self.logger().info(f"{name} Gas estimate:" - f" limit = {self._gas_limits[name]:.0f}," - f" price = {self.gas_level.name}," - f" estimated cost = {get_gas_price(False) * self._gas_limits[name]:.5f} ETH") - await asyncio.sleep(self.refresh_time) - except asyncio.CancelledError: - raise - except Exception: - self.logger().network("Unexpected error running logging task.", exc_info=True) - await asyncio.sleep(self.refresh_time) - - async def start_network(self): - self._async_task = safe_ensure_future(self.gas_price_update_loop()) - - async def stop_network(self): - if self._async_task is not None: - self._async_task.cancel() - self._async_task = None - - async def check_network(self) -> NetworkStatus: - try: - url = ETH_GASSTATION_API_URL.format(self.api_key) - async with aiohttp.ClientSession() as client: - response = await client.get(url=url) - if response.status != 200: - raise Exception(f"Error connecting to {url}. HTTP status is {response.status}.") - except asyncio.CancelledError: - raise - except Exception: - return NetworkStatus.NOT_CONNECTED - return NetworkStatus.CONNECTED - - def start(self): - NetworkBase.start(self) - - def stop(self): - NetworkBase.stop(self) diff --git a/hummingbot/core/utils/ethereum.py b/hummingbot/core/utils/ethereum.py index ff667d3a0a..e30c2b3478 100644 --- a/hummingbot/core/utils/ethereum.py +++ b/hummingbot/core/utils/ethereum.py @@ -3,7 +3,11 @@ from hexbytes import HexBytes from web3 import Web3 from web3.datastructures import AttributeDict -from typing import Dict +from typing import Dict, List +import aiohttp +from hummingbot.client.config.global_config_map import global_config_map +import itertools as it +from hummingbot.core.utils import async_ttl_cache def check_web3(ethereum_rpc_url: str) -> bool: @@ -32,3 +36,55 @@ def block_values_to_hex(block: AttributeDict) -> AttributeDict: except binascii.Error: formatted_block[key] = value return AttributeDict(formatted_block) + + +def check_transaction_exceptions(trade_data: dict) -> dict: + + exception_list = [] + + gas_limit = trade_data["gas_limit"] + # gas_price = trade_data["gas_price"] + gas_cost = trade_data["gas_cost"] + amount = trade_data["amount"] + side = trade_data["side"] + base = trade_data["base"] + quote = trade_data["quote"] + balances = trade_data["balances"] + allowances = trade_data["allowances"] + swaps_message = f"Total swaps: {trade_data['swaps']}" if "swaps" in trade_data.keys() else '' + + eth_balance = balances["ETH"] + + # check for sufficient gas + if eth_balance < gas_cost: + exception_list.append(f"Insufficient ETH balance to cover gas:" + f" Balance: {eth_balance}. Est. gas cost: {gas_cost}. {swaps_message}") + + trade_token = base if side == "side" else quote + trade_allowance = allowances[trade_token] + + # check for gas limit set to low + gas_limit_threshold = 21000 + if gas_limit < gas_limit_threshold: + exception_list.append(f"Gas limit {gas_limit} below recommended {gas_limit_threshold} threshold.") + + # check for insufficient token allowance + if allowances[trade_token] < amount: + exception_list.append(f"Insufficient {trade_token} allowance {trade_allowance}. Amount to trade: {amount}") + + return exception_list + + +@async_ttl_cache(ttl=30) +async def fetch_trading_pairs() -> List[str]: + token_list_url = global_config_map.get("ethereum_token_list_url").value + tokens = set() + async with aiohttp.ClientSession() as client: + resp = await client.get(token_list_url) + resp_json = await resp.json() + for token in resp_json["tokens"]: + tokens.add(token["symbol"]) + trading_pairs = [] + for base, quote in it.permutations(tokens, 2): + trading_pairs.append(f"{base}-{quote}") + return trading_pairs diff --git a/hummingbot/strategy/amm_arb/amm_arb.py b/hummingbot/strategy/amm_arb/amm_arb.py index b96af808fc..381cd991cc 100644 --- a/hummingbot/strategy/amm_arb/amm_arb.py +++ b/hummingbot/strategy/amm_arb/amm_arb.py @@ -253,13 +253,18 @@ async def format_status(self) -> str: market, trading_pair, base_asset, quote_asset = market_info buy_price = await market.get_quote_price(trading_pair, True, self._order_amount) sell_price = await market.get_quote_price(trading_pair, False, self._order_amount) - mid_price = (buy_price + sell_price) / 2 + + # check for unavailable price data + buy_price = Decimal(str(buy_price)) if buy_price is not None else '-' + sell_price = Decimal(str(sell_price)) if sell_price is not None else '-' + mid_price = ((buy_price + sell_price) / 2) if '-' not in [buy_price, sell_price] else '-' + data.append([ market.display_name, trading_pair, - float(sell_price), - float(buy_price), - float(mid_price) + sell_price, + buy_price, + mid_price ]) markets_df = pd.DataFrame(data=data, columns=columns) lines = [] @@ -335,7 +340,7 @@ async def quote_in_eth_rate_fetch_loop(self): self._market_2_quote_eth_rate = await self.request_rate_in_eth(self._market_info_2.quote_asset) self.logger().warning(f"Estimate conversion rate - " f"{self._market_info_2.quote_asset}:ETH = {self._market_2_quote_eth_rate} ") - await asyncio.sleep(60 * 5) + await asyncio.sleep(60 * 1) except asyncio.CancelledError: raise except Exception as e: @@ -348,4 +353,5 @@ async def quote_in_eth_rate_fetch_loop(self): async def request_rate_in_eth(self, quote: str) -> int: if self._uniswap is None: self._uniswap = UniswapConnector([f"{quote}-WETH"], "", None) + await self._uniswap.initiate_pool() # initiate to cache swap pool return await self._uniswap.get_quote_price(f"{quote}-WETH", True, 1) diff --git a/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py b/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py index 1501d16c6c..e488ec0f17 100644 --- a/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py +++ b/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py @@ -100,6 +100,11 @@ def validate_price_floor_ceiling(value: str) -> Optional[str]: return "Value must be more than 0 or -1 to disable this feature." +def on_validated_price_type(value: str): + if value == 'inventory_cost': + pure_market_making_config_map["inventory_price"].value = None + + def exchange_on_validated(value: str): required_exchanges.append(value) @@ -241,6 +246,7 @@ def exchange_on_validated(value: str): prompt="What is the price of your base asset inventory? ", type_str="decimal", validator=lambda v: validate_decimal(v, min_value=Decimal("0"), inclusive=True), + required_if=lambda: pure_market_making_config_map.get("price_type").value == "inventory_cost", default=Decimal("1"), ), "filled_order_delay": @@ -308,6 +314,7 @@ def exchange_on_validated(value: str): type_str="str", required_if=lambda: pure_market_making_config_map.get("price_source").value != "custom_api", default="mid_price", + on_validated=on_validated_price_type, validator=lambda s: None if s in {"mid_price", "last_price", "last_own_trade_price", diff --git a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml index ed08e2fa90..3aff911e85 100644 --- a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml +++ b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml @@ -72,9 +72,15 @@ okex_taker_fee: balancer_maker_fee_amount: balancer_taker_fee_amount: +uniswap_maker_fee_amount: +uniswap_taker_fee_amount: + bitmax_maker_fee: bitmax_taker_fee: +ascend_ex_maker_fee: +ascend_ex_taker_fee: + probit_maker_fee: probit_taker_fee: diff --git a/hummingbot/templates/conf_global_TEMPLATE.yml b/hummingbot/templates/conf_global_TEMPLATE.yml index cb459640eb..84946fb27f 100644 --- a/hummingbot/templates/conf_global_TEMPLATE.yml +++ b/hummingbot/templates/conf_global_TEMPLATE.yml @@ -69,8 +69,8 @@ okex_api_key: null okex_secret_key: null okex_passphrase: null -bitmax_api_key: null -bitmax_secret_key: null +ascend_ex_api_key: null +ascend_ex_secret_key: null celo_address: null celo_password: null @@ -94,7 +94,7 @@ ethereum_wallet: null ethereum_rpc_url: null ethereum_rpc_ws_url: null ethereum_chain_name: MAIN_NET -ethereum_token_list_url: null +ethereum_token_list_url: https://wispy-bird-88a7.uniswap.workers.dev/?url=http://tokens.1inch.eth.link # Kill switch kill_switch_enabled: null @@ -175,14 +175,7 @@ balance_asset_limit: # Fixed gas price (in Gwei) for Ethereum transactions manual_gas_price: -# To enable gas price lookup (true/false) -ethgasstation_gas_enabled: -# API key for defipulse.com gas station API -ethgasstation_api_key: -# Gas level you want to use for Ethereum transactions (fast, fastest, safeLow, average) -ethgasstation_gas_level: -# Refresh time for Ethereum gas price lookups (in seconds) -ethgasstation_refresh_time: + # Gateway API Configurations # default host to only use localhost # Port need to match the final installation port for Gateway diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index c6db0bae8c..49fc98579c 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -40,7 +40,7 @@ else else echo "‼️ hummingbot_conf & hummingbot_certs directory missing from path $FOLDER" prompt_hummingbot_data_path - fi + fi if [[ -f "$FOLDER/hummingbot_certs/server_cert.pem" && -f "$FOLDER/hummingbot_certs/server_key.pem" && -f "$FOLDER/hummingbot_certs/ca_cert.pem" ]]; then echo @@ -79,70 +79,208 @@ do then HUMMINGBOT_INSTANCE_ID="$(echo -e "${value}" | tr -d '[:space:]')" fi - # chain - if [ "$key" == "ethereum_chain_name" ] + # +done < "$GLOBAL_CONFIG" +} +read_global_config + +# prompt to setup balancer, uniswap +prompt_ethereum_setup () { + read -p " Do you want to setup Balancer or Uniswap? [Y/N] (default \"Y\") >>> " PROCEED + if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" || "$PROCEED" == "" ]] then - ETHEREUM_CHAIN="$(echo -e "${value}" | tr -d '[:space:]')" - # subgraph url - if [[ "$ETHEREUM_CHAIN" == "MAIN_NET" || "$ETHEREUM_CHAIN" == "main_net" || "$ETHEREUM_CHAIN" == "MAINNET" || "$ETHEREUM_CHAIN" == "mainnet" ]] + ETHEREUM_SETUP=true + echo + read -p " Enter Ethereum chain you want to use [mainnet/kovan] (default = \"mainnet\") >>> " ETHEREUM_CHAIN + # chain selection + if [ "$ETHEREUM_CHAIN" == "" ] + then + ETHEREUM_CHAIN="mainnet" + fi + if [[ "$ETHEREUM_CHAIN" != "mainnet" && "$ETHEREUM_CHAIN" != "kovan" ]] + then + echo "‼️ ERROR. Unsupported chains (mainnet/kovan). " + prompt_ethereum_setup + fi + # set subgraph url, exchange_proxy + if [[ "$ETHEREUM_CHAIN" == "mainnet" ]] then ETHEREUM_CHAIN="mainnet" REACT_APP_SUBGRAPH_URL="https://api.thegraph.com/subgraphs/name/balancer-labs/balancer" EXCHANGE_PROXY="0x3E66B66Fd1d0b02fDa6C811Da9E0547970DB2f21" else - ETHEREUM_CHAIN="kovan" - REACT_APP_SUBGRAPH_URL="https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-kovan" - EXCHANGE_PROXY="0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec" + if [[ "$ETHEREUM_CHAIN" == "kovan" ]] + then + REACT_APP_SUBGRAPH_URL="https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-kovan" + EXCHANGE_PROXY="0x4e67bf5bD28Dd4b570FBAFe11D0633eCbA2754Ec" + fi fi fi - # ethereum rpc url - if [ "$key" == "ethereum_rpc_url" ] +} +prompt_ethereum_setup + +# prompt to ethereum rpc +prompt_ethereum_rpc_setup () { + if [ "$ETHEREUM_RPC_URL" == "" ] then - ETHEREUM_RPC_URL="$(echo -e "${value}" | tr -d '[:space:]')" + read -p " Enter the Ethereum RPC node URL to connect to >>> " ETHEREUM_RPC_URL + if [ "$ETHEREUM_RPC_URL" == "" ] + then + prompt_ethereum_rpc_setup + fi + else + read -p " Use the this Ethereum RPC node ($ETHEREUM_RPC_URL) setup in Hummingbot client? [Y/N] (default = \"Y\") >>> " PROCEED + if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" || "$PROCEED" == "" ]] + then + echo + else + ETHEREUM_RPC_URL="" + prompt_ethereum_rpc_setup + fi fi -done < "$GLOBAL_CONFIG" } -read_global_config +prompt_ethereum_rpc_setup -# prompt to setup balancer, uniswap -prompt_ethereum_setup () { - read -p " Do you want to setup Balancer/Uniswap/Perpetual Finance? [Y/N] >>> " PROCEED - if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" ]] +# prompt to setup ethereum token list +prompt_token_list_source () { + echo + echo " Enter the token list url available at https://tokenlists.org/" + read -p " (default = \"https://wispy-bird-88a7.uniswap.workers.dev/?url=http://tokens.1inch.eth.link\") >>> " ETHEREUM_TOKEN_LIST_URL + if [ "$ETHEREUM_TOKEN_LIST_URL" == "" ] then + echo echo "ℹ️ Retrieving config from Hummingbot config file ... " ETHEREUM_SETUP=true + ETHEREUM_TOKEN_LIST_URL=https://wispy-bird-88a7.uniswap.workers.dev/?url=http://tokens.1inch.eth.link + fi +} +prompt_token_list_source + +# prompt to setup eth gas level +prompt_eth_gasstation_gas_level () { + echo + read -p " Enter gas level you want to use for Ethereum transactions (fast, fastest, safeLow, average) (default = \"fast\") >>> " ETH_GAS_STATION_GAS_LEVEL + if [ "$ETH_GAS_STATION_GAS_LEVEL" == "" ] + then + ETH_GAS_STATION_GAS_LEVEL=fast + else + if [[ "$ETH_GAS_STATION_GAS_LEVEL" != "fast" && "$ETH_GAS_STATION_GAS_LEVEL" != "fastest" && "$ETH_GAS_STATION_GAS_LEVEL" != "safeLow" && "$ETH_GAS_STATION_GAS_LEVEL" != "safelow" && "$ETH_GAS_STATION_GAS_LEVEL" != "average" ]] + then + prompt_eth_gasstation_gas_level + fi + fi +} + +# prompt to setup eth gas station +prompt_eth_gasstation_setup () { + echo + read -p " Enable dynamic Ethereum gas price lookup? [Y/N] (default = \"Y\") >>> " PROCEED + if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" || "$PROCEED" == "" ]] + then + ENABLE_ETH_GAS_STATION=true + read -p " Enter API key for Eth Gas Station (https://ethgasstation.info/) >>> " ETH_GAS_STATION_API_KEY + if [ "$ETH_GAS_STATION_API_KEY" == "" ] + then + prompt_eth_gasstation_setup + else + # set gas level + prompt_eth_gasstation_gas_level + + # set refresh interval + read -p " Enter refresh time for Ethereum gas price lookup (in seconds) (default = \"120\") >>> " ETH_GAS_STATION_REFRESH_TIME + if [ "$ETH_GAS_STATION_REFRESH_TIME" == "" ] + then + ETH_GAS_STATION_REFRESH_TIME=120 + fi + fi + else + if [[ "$PROCEED" == "N" || "$PROCEED" == "n" ]] + then + ENABLE_ETH_GAS_STATION=false + # set manual gas price + read -p " Enter fixed gas price (in Gwei) you want to use for Ethereum transactions (default = \"100\") >>> " MANUAL_GAS_PRICE + if [ "$MANUAL_GAS_PRICE" == "" ] + then + MANUAL_GAS_PRICE=100 + fi + else + prompt_eth_gasstation_setup + fi + fi + echo +} +prompt_eth_gasstation_setup + +prompt_balancer_setup () { + # Ask the user for the Balancer specific settings + echo "ℹ️ Balancer setting " + read -p " Enter the maximum Balancer swap pool (default = \"4\") >>> " BALANCER_MAX_SWAPS + if [ "$BALANCER_MAX_SWAPS" == "" ] + then + BALANCER_MAX_SWAPS="4" echo fi } -prompt_ethereum_setup -# Ask the user for ethereum network -prompt_terra_network () { -read -p " Enter Terra chain you want to use [mainnet/testnet] (default = \"mainnet\") >>> " TERRA -# chain selection -if [ "$TERRA" == "" ] -then - TERRA="mainnet" -fi -if [[ "$TERRA" != "mainnet" && "$TERRA" != "testnet" ]] -then - echo "‼️ ERROR. Unsupported chains (mainnet/testnet). " - prompt_terra_network -fi -# setup chain params -if [[ "$TERRA" == "mainnet" ]] -then - TERRA_LCD_URL="https://lcd.terra.dev" - TERRA_CHAIN="columbus-4" -elif [ "$TERRA" == "testnet" ] +prompt_uniswap_setup () { + # Ask the user for the Uniswap specific settings + echo "ℹ️ Uniswap setting " + read -p " Enter the allowed slippage for swap transactions (default = \"1.5\") >>> " UNISWAP_SLIPPAGE + if [ "$UNISWAP_SLIPPAGE" == "" ] + then + UNISWAP_SLIPPAGE="1.5" + echo + fi +} + +if [[ "$ETHEREUM_SETUP" == true ]] then - TERRA_LCD_URL="https://tequila-lcd.terra.dev" - TERRA_CHAIN="tequila-0004" + prompt_balancer_setup + prompt_uniswap_setup fi + +prompt_xdai_setup () { + # Ask the user for the Uniswap specific settings + echo "ℹ️ XDAI setting " + read -p " Enter preferred XDAI rpc provider (default = \"https://rpc.xdaichain.com\") >>> " XDAI_PROVIDER + if [ "$XDAI_PROVIDER" == "" ] + then + XDAI_PROVIDER="https://rpc.xdaichain.com" + echo + fi } +prompt_xdai_setup + +# Ask the user for ethereum network +prompt_terra_network () { + echo + read -p " Enter Terra chain you want to use [mainnet/testnet] (default = \"mainnet\") >>> " TERRA + # chain selection + if [ "$TERRA" == "" ] + then + TERRA="mainnet" + fi + if [[ "$TERRA" != "mainnet" && "$TERRA" != "testnet" ]] + then + echo "‼️ ERROR. Unsupported chains (mainnet/testnet). " + prompt_terra_network + fi + # setup chain params + if [[ "$TERRA" == "mainnet" ]] + then + TERRA_LCD_URL="https://lcd.terra.dev" + TERRA_CHAIN="columbus-4" + elif [ "$TERRA" == "testnet" ] + then + TERRA_LCD_URL="https://tequila-lcd.terra.dev" + TERRA_CHAIN="tequila-0004" + fi +} + prompt_terra_setup () { - read -p " Do you want to setup Terra? [Y/N] >>> " PROCEED - if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" ]] + echo + read -p " Do you want to setup Terra? [Y/N] (default \"Y\") >>> " PROCEED + if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" || "$PROCEED" == "" ]] then TERRA_SETUP=true prompt_terra_network @@ -202,9 +340,17 @@ echo printf "%30s %5s\n" "Hummingbot Instance ID:" "$HUMMINGBOT_INSTANCE_ID" printf "%30s %5s\n" "Ethereum Chain:" "$ETHEREUM_CHAIN" printf "%30s %5s\n" "Ethereum RPC URL:" "$ETHEREUM_RPC_URL" +printf "%30s %5s\n" "Ethereum Token List URL:" "$ETHEREUM_TOKEN_LIST_URL" +printf "%30s %5s\n" "Manual Gas Price:" "$MANUAL_GAS_PRICE" +printf "%30s %5s\n" "Enable Eth Gas Station:" "$ENABLE_ETH_GAS_STATION" +printf "%30s %5s\n" "Eth Gas Station API:" "$ETH_GAS_STATION_API_KEY" +printf "%30s %5s\n" "Eth Gas Station Level:" "$ETH_GAS_STATION_GAS_LEVEL" +printf "%30s %5s\n" "Eth Gas Station Refresh Interval:" "$ETH_GAS_STATION_REFRESH_TIME" printf "%30s %5s\n" "Balancer Subgraph:" "$REACT_APP_SUBGRAPH_URL" printf "%30s %5s\n" "Balancer Exchange Proxy:" "$EXCHANGE_PROXY" +printf "%30s %5s\n" "Balancer Max Swaps:" "$BALANCER_MAX_SWAPS" printf "%30s %5s\n" "Uniswap Router:" "$UNISWAP_ROUTER" +printf "%30s %5s\n" "Uniswap Allowed Slippage:" "$UNISWAP_SLIPPAGE" printf "%30s %5s\n" "Terra Chain:" "$TERRA" printf "%30s %5s\n" "Gateway Log Path:" "$LOG_PATH" printf "%30s %5s\n" "Gateway Cert Path:" "$CERT_PATH" @@ -220,13 +366,46 @@ echo "NODE_ENV=prod" >> $ENV_FILE echo "PORT=$PORT" >> $ENV_FILE echo "" >> $ENV_FILE echo "HUMMINGBOT_INSTANCE_ID=$HUMMINGBOT_INSTANCE_ID" >> $ENV_FILE + +# ethereum config +echo "" >> $ENV_FILE +echo "# Ethereum Settings" >> $ENV_FILE echo "ETHEREUM_CHAIN=$ETHEREUM_CHAIN" >> $ENV_FILE echo "ETHEREUM_RPC_URL=$ETHEREUM_RPC_URL" >> $ENV_FILE +echo "ETHEREUM_TOKEN_LIST_URL=$ETHEREUM_TOKEN_LIST_URL" >> $ENV_FILE +echo "" >> $ENV_FILE +echo "ENABLE_ETH_GAS_STATION=$ENABLE_ETH_GAS_STATION" >> $ENV_FILE +echo "ETH_GAS_STATION_API_KEY=$ETH_GAS_STATION_API_KEY" >> $ENV_FILE +echo "ETH_GAS_STATION_GAS_LEVEL=$ETH_GAS_STATION_GAS_LEVEL" >> $ENV_FILE +echo "ETH_GAS_STATION_REFRESH_TIME=$ETH_GAS_STATION_REFRESH_TIME" >> $ENV_FILE +echo "MANUAL_GAS_PRICE=$MANUAL_GAS_PRICE" >> $ENV_FILE + +# balancer config +echo "" >> $ENV_FILE +echo "# Balancer Settings" >> $ENV_FILE echo "REACT_APP_SUBGRAPH_URL=$REACT_APP_SUBGRAPH_URL" >> $ENV_FILE # must used "REACT_APP_SUBGRAPH_URL" for balancer-sor echo "EXCHANGE_PROXY=$EXCHANGE_PROXY" >> $ENV_FILE +echo "BALANCER_MAX_SWAPS=$BALANCER_MAX_SWAPS" >> $ENV_FILE + +# uniswap config +echo "" >> $ENV_FILE +echo "# Uniswap Settings" >> $ENV_FILE echo "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE +echo "UNISWAP_ALLOWED_SLIPPAGE=$UNISWAP_SLIPPAGE" >> $ENV_FILE +echo "UNISWAP_NO_RESERVE_CHECK_INTERVAL=300000" >> $ENV_FILE +echo "UNISWAP_PAIRS_CACHE_TIME=1000" >> $ENV_FILE + +# terra config +echo "" >> $ENV_FILE +echo "# Terra Settings" >> $ENV_FILE echo "TERRA_LCD_URL=$TERRA_LCD_URL" >> $ENV_FILE echo "TERRA_CHAIN=$TERRA_CHAIN" >> $ENV_FILE + +# perpeptual finance config +echo "" >> $ENV_FILE +echo "# Perpeptual Settings" >> $ENV_FILE +echo "XDAI_PROVIDER=$XDAI_PROVIDER" >> $ENV_FILE + echo "" >> $ENV_FILE prompt_proceed () { @@ -234,7 +413,12 @@ prompt_proceed () { read -p " Do you want to proceed with installation? [Y/N] >>> " PROCEED if [ "$PROCEED" == "" ] then - PROCEED="Y" + prompt_proceed + else + if [[ "$PROCEED" != "Y" && "$PROCEED" != "y" ]] + then + PROCEED="N" + fi fi } diff --git a/setup.py b/setup.py index 083859ad4c..6ad90e8f60 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ def main(): "hummingbot.connector.connector.balancer", "hummingbot.connector.connector.terra", "hummingbot.connector.exchange", + "hummingbot.connector.exchange.ascend_ex", "hummingbot.connector.exchange.binance", "hummingbot.connector.exchange.bitfinex", "hummingbot.connector.exchange.bittrex", @@ -56,7 +57,6 @@ def main(): "hummingbot.connector.exchange.dolomite", "hummingbot.connector.exchange.eterbase", "hummingbot.connector.exchange.beaxy", - "hummingbot.connector.exchange.bitmax", "hummingbot.connector.exchange.hitbtc", "hummingbot.connector.derivative", "hummingbot.connector.derivative.binance_perpetual", diff --git a/test/connector/connector/balancer/test_balancer_connector.py b/test/connector/connector/balancer/test_balancer_connector.py index 151d0e403e..66ad53df97 100644 --- a/test/connector/connector/balancer/test_balancer_connector.py +++ b/test/connector/connector/balancer/test_balancer_connector.py @@ -28,8 +28,7 @@ global_config_map['gateway_api_host'].value = "localhost" global_config_map['gateway_api_port'].value = 5000 -global_config_map['ethgasstation_gas_enabled'].value = False -global_config_map['manual_gas_price'].value = 50 +global_config_map['ethereum_token_list_url'].value = "https://defi.cmc.eth.link" global_config_map.get("ethereum_chain_name").value = "kovan" trading_pair = "WETH-DAI" @@ -96,6 +95,14 @@ def setUp(self): for event_tag in self.events: self.connector.add_listener(event_tag, self.event_logger) + def test_fetch_trading_pairs(self): + asyncio.get_event_loop().run_until_complete(self._test_fetch_trading_pairs()) + + async def _test_fetch_trading_pairs(self): + pairs = await BalancerConnector.fetch_trading_pairs() + print(pairs) + self.assertGreater(len(pairs), 0) + def test_update_balances(self): all_bals = self.connector.get_all_balances() for token, bal in all_bals.items(): diff --git a/test/connector/exchange/bitmax/.gitignore b/test/connector/exchange/ascend_ex/.gitignore similarity index 100% rename from test/connector/exchange/bitmax/.gitignore rename to test/connector/exchange/ascend_ex/.gitignore diff --git a/test/connector/exchange/bitmax/__init__.py b/test/connector/exchange/ascend_ex/__init__.py similarity index 100% rename from test/connector/exchange/bitmax/__init__.py rename to test/connector/exchange/ascend_ex/__init__.py diff --git a/test/connector/exchange/bitmax/test_bitmax_auth.py b/test/connector/exchange/ascend_ex/test_ascend_ex_auth.py similarity index 72% rename from test/connector/exchange/bitmax/test_bitmax_auth.py rename to test/connector/exchange/ascend_ex/test_ascend_ex_auth.py index 46a68bfae7..59d866ad87 100644 --- a/test/connector/exchange/bitmax/test_bitmax_auth.py +++ b/test/connector/exchange/ascend_ex/test_ascend_ex_auth.py @@ -9,21 +9,22 @@ import logging from os.path import join, realpath from typing import Dict, Any -from hummingbot.connector.exchange.bitmax.bitmax_auth import BitmaxAuth +from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL -from hummingbot.connector.exchange.bitmax.bitmax_constants import REST_URL, getWsUrlPriv +from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import REST_URL +from hummingbot.connector.exchange.ascend_ex.ascend_ex_util import get_rest_url_private sys.path.insert(0, realpath(join(__file__, "../../../../../"))) logging.basicConfig(level=METRICS_LOG_LEVEL) -class TestAuth(unittest.TestCase): +class TestAscendExAuth(unittest.TestCase): @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() - api_key = conf.bitmax_api_key - secret_key = conf.bitmax_secret_key - cls.auth = BitmaxAuth(api_key, secret_key) + api_key = conf.ascend_ex_api_key + secret_key = conf.ascend_ex_secret_key + cls.auth = AscendExAuth(api_key, secret_key) async def rest_auth(self) -> Dict[Any, Any]: headers = { @@ -37,7 +38,7 @@ async def ws_auth(self) -> Dict[Any, Any]: info = await self.rest_auth() accountGroup = info.get("data").get("accountGroup") headers = self.auth.get_auth_headers("stream") - ws = await websockets.connect(f"{getWsUrlPriv(accountGroup)}/stream", extra_headers=headers) + ws = await websockets.connect(f"{get_rest_url_private(accountGroup)}/stream", extra_headers=headers) raw_msg = await asyncio.wait_for(ws.recv(), 5000) msg = ujson.loads(raw_msg) diff --git a/test/connector/exchange/bitmax/test_bitmax_exchange.py b/test/connector/exchange/ascend_ex/test_ascend_ex_exchange.py similarity index 97% rename from test/connector/exchange/bitmax/test_bitmax_exchange.py rename to test/connector/exchange/ascend_ex/test_ascend_ex_exchange.py index ca642fe385..e3c928520d 100644 --- a/test/connector/exchange/bitmax/test_bitmax_exchange.py +++ b/test/connector/exchange/ascend_ex/test_ascend_ex_exchange.py @@ -1,15 +1,17 @@ from os.path import join, realpath import sys; sys.path.insert(0, realpath(join(__file__, "../../../../../"))) + import asyncio -import logging -from decimal import Decimal -import unittest +import conf import contextlib -import time +import logging +import math import os +import time +import unittest + +from decimal import Decimal from typing import List -import conf -import math from hummingbot.core.clock import Clock, ClockMode from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL @@ -33,15 +35,15 @@ from hummingbot.model.order import Order from hummingbot.model.trade_fill import TradeFill from hummingbot.connector.markets_recorder import MarketsRecorder -from hummingbot.connector.exchange.bitmax.bitmax_exchange import BitmaxExchange +from hummingbot.connector.exchange.ascend_ex.ascend_ex_exchange import AscendExExchange logging.basicConfig(level=METRICS_LOG_LEVEL) -API_KEY = conf.bitmax_api_key -API_SECRET = conf.bitmax_secret_key +API_KEY = conf.ascend_ex_api_key +API_SECRET = conf.ascend_ex_secret_key -class BitmaxExchangeUnitTest(unittest.TestCase): +class AscendExExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, @@ -52,7 +54,7 @@ class BitmaxExchangeUnitTest(unittest.TestCase): MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] - connector: BitmaxExchange + connector: AscendExExchange event_logger: EventLogger trading_pair = "BTC-USDT" base_token, quote_token = trading_pair.split("-") @@ -65,13 +67,13 @@ def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) - cls.connector: BitmaxExchange = BitmaxExchange( - bitmax_api_key=API_KEY, - bitmax_secret_key=API_SECRET, + cls.connector: AscendExExchange = AscendExExchange( + ascend_ex_api_key=API_KEY, + ascend_ex_secret_key=API_SECRET, trading_pairs=[cls.trading_pair], trading_required=True ) - print("Initializing Bitmax market... this will take about a minute.") + print("Initializing AscendEx exchange... this will take about a minute.") cls.clock.add_iterator(cls.connector) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) @@ -336,7 +338,7 @@ def test_orders_saving_and_restoration(self): self.clock.remove_iterator(self.connector) for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) - new_connector = BitmaxExchange(API_KEY, API_SECRET, [self.trading_pair], True) + new_connector = AscendExExchange(API_KEY, API_SECRET, [self.trading_pair], True) for event_tag in self.events: new_connector.add_listener(event_tag, self.event_logger) recorder.stop() diff --git a/test/connector/exchange/bitmax/test_bitmax_order_book_tracker.py b/test/connector/exchange/ascend_ex/test_ascend_ex_order_book_tracker.py similarity index 88% rename from test/connector/exchange/bitmax/test_bitmax_order_book_tracker.py rename to test/connector/exchange/ascend_ex/test_ascend_ex_order_book_tracker.py index 331860fbba..ed19775c0d 100755 --- a/test/connector/exchange/bitmax/test_bitmax_order_book_tracker.py +++ b/test/connector/exchange/ascend_ex/test_ascend_ex_order_book_tracker.py @@ -9,8 +9,8 @@ from typing import Dict, Optional, List from hummingbot.core.event.event_logger import EventLogger from hummingbot.core.event.events import OrderBookEvent, OrderBookTradeEvent, TradeType -from hummingbot.connector.exchange.bitmax.bitmax_order_book_tracker import BitmaxOrderBookTracker -from hummingbot.connector.exchange.bitmax.bitmax_api_order_book_data_source import BitmaxAPIOrderBookDataSource +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book_tracker import AscendExOrderBookTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source import AscendExAPIOrderBookDataSource from hummingbot.core.data_type.order_book import OrderBook from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL @@ -19,8 +19,8 @@ logging.basicConfig(level=METRICS_LOG_LEVEL) -class BitmaxOrderBookTrackerUnitTest(unittest.TestCase): - order_book_tracker: Optional[BitmaxOrderBookTracker] = None +class AscendExOrderBookTrackerUnitTest(unittest.TestCase): + order_book_tracker: Optional[AscendExOrderBookTracker] = None events: List[OrderBookEvent] = [ OrderBookEvent.TradeEvent ] @@ -32,7 +32,7 @@ class BitmaxOrderBookTrackerUnitTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() - cls.order_book_tracker: BitmaxOrderBookTracker = BitmaxOrderBookTracker(cls.trading_pairs) + cls.order_book_tracker: AscendExOrderBookTracker = AscendExOrderBookTracker(cls.trading_pairs) cls.order_book_tracker.start() cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @@ -96,7 +96,7 @@ def test_tracker_integrity(self): def test_api_get_last_traded_prices(self): prices = self.ev_loop.run_until_complete( - BitmaxAPIOrderBookDataSource.get_last_traded_prices(["BTC-USDT", "LTC-BTC"])) + AscendExAPIOrderBookDataSource.get_last_traded_prices(["BTC-USDT", "LTC-BTC"])) for key, value in prices.items(): print(f"{key} last_trade_price: {value}") self.assertGreater(prices["BTC-USDT"], 1000) diff --git a/test/connector/exchange/bitmax/test_bitmax_user_stream_tracker.py b/test/connector/exchange/ascend_ex/test_ascend_ex_user_stream_tracker.py similarity index 58% rename from test/connector/exchange/bitmax/test_bitmax_user_stream_tracker.py rename to test/connector/exchange/ascend_ex/test_ascend_ex_user_stream_tracker.py index 1b4cb5c84b..ce107d09f2 100644 --- a/test/connector/exchange/bitmax/test_bitmax_user_stream_tracker.py +++ b/test/connector/exchange/ascend_ex/test_ascend_ex_user_stream_tracker.py @@ -6,8 +6,8 @@ import conf from os.path import join, realpath -from hummingbot.connector.exchange.bitmax.bitmax_user_stream_tracker import BitmaxUserStreamTracker -from hummingbot.connector.exchange.bitmax.bitmax_auth import BitmaxAuth +from hummingbot.connector.exchange.ascend_ex.ascend_ex_user_stream_tracker import AscendExUserStreamTracker +from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL @@ -16,17 +16,17 @@ logging.basicConfig(level=METRICS_LOG_LEVEL) -class BitmaxUserStreamTrackerUnitTest(unittest.TestCase): - api_key = conf.bitmax_api_key - api_secret = conf.bitmax_secret_key +class AscendExUserStreamTrackerUnitTest(unittest.TestCase): + api_key = conf.ascend_ex_api_key + api_secret = conf.ascend_ex_secret_key @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() - cls.bitmax_auth = BitmaxAuth(cls.api_key, cls.api_secret) + cls.ascend_ex_auth = AscendExAuth(cls.api_key, cls.api_secret) cls.trading_pairs = ["BTC-USDT"] - cls.user_stream_tracker: BitmaxUserStreamTracker = BitmaxUserStreamTracker( - bitmax_auth=cls.bitmax_auth, trading_pairs=cls.trading_pairs) + cls.user_stream_tracker: AscendExUserStreamTracker = AscendExUserStreamTracker( + ascend_ex_auth=cls.ascend_ex_auth, trading_pairs=cls.trading_pairs) cls.user_stream_tracker_task: asyncio.Task = safe_ensure_future(cls.user_stream_tracker.start()) def test_user_stream(self):