From dc485e3a49cd2bb6cf625cc7ecb06a160c4bd8e6 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 1 Feb 2021 19:19:56 +0800 Subject: [PATCH 01/34] (feat) Update Gateway endpoints & parameter formats * Add protocols swap pool caching on start --- .../connector/balancer/balancer_connector.py | 57 ++++++++++++------- .../connector/terra/terra_connector.py | 6 +- .../connector/uniswap/uniswap_connector.py | 36 ++++++------ .../core/utils/eth_gas_station_lookup.py | 2 +- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index e1bc1e131d..fff00f61b2 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -88,6 +88,7 @@ def __init__(self, self._allowances = {} self._status_polling_task = None self._auto_approve_task = None + self._initiate_pool_task = None self._real_time_balance_update = False self._max_swaps = global_config_map['balancer_max_swaps'].value self._poll_notifier = None @@ -115,6 +116,21 @@ def limit_orders(self) -> List[LimitOrder]: for in_flight_order in self._in_flight_orders.values() ] + async def initiate_pool(self) -> str: + """ + Initiate to cache pools and auto approve allowances for token in trading_pairs + :return: A success/fail status for initiation + """ + self.logger().info("Initializing strategy and caching swap pools ...") + base, quote = self._trading_pairs[0].split("-") + resp = await self._api_request("post", "eth/balancer/start", + {"base": base, + "quote": quote, + "gasPrice": str(get_gas_price()) + }) + status = resp["success"] + return status + async def auto_approve(self): """ Automatically approves Balancer contract as a spender for token in trading pairs. @@ -138,9 +154,8 @@ async def approve_balancer_spender(self, token_symbol: str) -> Decimal: """ resp = await self._api_request("post", "eth/approve", - {"tokenAddress": self._token_addresses[token_symbol], + {"token": 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 "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -155,9 +170,8 @@ 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(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", "connector": self.name}) for address, amount in resp["approvals"].items(): ret_val[self.get_token(address)] = Decimal(str(amount)) @@ -177,13 +191,11 @@ 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}) + "side": side}) if resp["price"] is not None: return Decimal(str(resp["price"])) except asyncio.CancelledError: @@ -256,18 +268,16 @@ async def _create_order(self, 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), + "limitPrice": str(price), "gasPrice": str(gas_price), - "base_decimals": self._token_decimals[base], - "quote_decimals": self._token_decimals[quote], } 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") tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: @@ -340,7 +350,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: @@ -429,6 +439,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 +449,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: @@ -492,9 +506,8 @@ 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(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]"}) for token, bal in resp_json["balances"].items(): if len(token) > 4: token = self.get_token(token) 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..1517241f60 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -88,6 +88,7 @@ def __init__(self, self._allowances = {} self._status_polling_task = None self._auto_approve_task = None + self._initiate_pool_task = None self._real_time_balance_update = False self._poll_notifier = None @@ -137,9 +138,8 @@ async def approve_uniswap_spender(self, token_symbol: str) -> Decimal: """ resp = await self._api_request("post", "eth/approve", - {"tokenAddress": self._token_addresses[token_symbol], + {"token": 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 "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -154,9 +154,8 @@ 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(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", "connector": self.name}) for address, amount in resp["approvals"].items(): ret_val[self.get_token(address)] = Decimal(str(amount)) @@ -176,9 +175,10 @@ 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"])) @@ -252,15 +252,16 @@ async def _create_order(self, 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), + "limitPrice": str(price), "gasPrice": str(gas_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") tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: @@ -333,7 +334,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: @@ -422,6 +423,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 +433,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: @@ -485,9 +490,8 @@ async def _update_balances(self): 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(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]"}) for token, bal in resp_json["balances"].items(): if len(token) > 4: token = self.get_token(token) diff --git a/hummingbot/core/utils/eth_gas_station_lookup.py b/hummingbot/core/utils/eth_gas_station_lookup.py index ee56502631..54674d54f3 100644 --- a/hummingbot/core/utils/eth_gas_station_lookup.py +++ b/hummingbot/core/utils/eth_gas_station_lookup.py @@ -41,7 +41,7 @@ def request_gas_limit(connector_name: str) -> int: balancer_max_swaps = global_config_map["balancer_max_swaps"].value base_url = ':'.join(['https://' + host, port]) - url = f"{base_url}/{connector_name}/gas-limit" + url = f"{base_url}/eth/{connector_name}/gas-limit" ca_certs = GATEAWAY_CA_CERT_PATH client_certs = (GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH) From ab3f0c96c6ad3ce9d1f12db1132347ef858daee1 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 4 Feb 2021 08:50:17 +0800 Subject: [PATCH 02/34] (fix) Uniswap initiate pool not defined. Fix docker install script --- .../connector/uniswap/uniswap_connector.py | 15 ++++++++ .../docker-commands/create-gateway.sh | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 1517241f60..5ee7352879 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -115,6 +115,21 @@ def limit_orders(self) -> List[LimitOrder]: for in_flight_order in self._in_flight_orders.values() ] + async def initiate_pool(self) -> str: + """ + Initiate to cache pools and auto approve allowances for token in trading_pairs + :return: A success/fail status for initiation + """ + self.logger().info("Initializing strategy and caching swap pools ...") + base, quote = self._trading_pairs[0].split("-") + resp = await self._api_request("post", "eth/uniswap/start", + {"base": base, + "quote": quote, + "gasPrice": str(get_gas_price()) + }) + status = resp["success"] + return status + async def auto_approve(self): """ Automatically approves Uniswap contract as a spender for token in trading pairs. diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index 647b8c7797..05b99bca00 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -100,6 +100,19 @@ do then ETHEREUM_RPC_URL="$(echo -e "${value}" | tr -d '[:space:]')" fi + # ethergas station config + if [ "$key" == "ethgasstation_api_key" ] + then + ETH_GAS_STATION_API_KEY="$(echo -e "${value}" | tr -d '[:space:]')" + fi + if [ "$key" == "ethgasstation_gas_level" ] + then + ETH_GAS_STATION_GAS_LEVEL="$(echo -e "${value}" | tr -d '[:space:]')" + fi + if [ "$key" == "ethgasstation_refresh_time" ] + then + ETH_GAS_STATION_REFRESH_TIME="$(echo -e "${value}" | tr -d '[:space:]')" + fi done < "$GLOBAL_CONFIG" } read_global_config @@ -116,6 +129,22 @@ prompt_ethereum_setup () { } prompt_ethereum_setup +prompt_balancer_setup () { + # Ask the user for the max balancer pool to use + read -p " Enter the maximum balancer swap pool (default = \"4\") >>> " BALANCER_MAX_SWAPS + if [ "$BALANCER_MAX_SWAPS" == "" ] + then + BALANCER_MAX_SWAPS="4" + echo + fi +} + + +if [[ "$ETHEREUM_SETUP" == true ]] +then + prompt_balancer_setup +fi + # Ask the user for ethereum network prompt_terra_network () { read -p " Enter Terra chain you want to use [mainnet/testnet] (default = \"mainnet\") >>> " TERRA @@ -227,6 +256,13 @@ echo "EXCHANGE_PROXY=$EXCHANGE_PROXY" >> $ENV_FILE echo "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE echo "TERRA_LCD_URL=$TERRA_LCD_URL" >> $ENV_FILE echo "TERRA_CHAIN=$TERRA_CHAIN" >> $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 "BALANCER_MAX_SWAPS=$BALANCER_MAX_SWAPS" >> $ENV_FILE + echo "" >> $ENV_FILE prompt_proceed () { From 42182b1292c1b6dc98b398dfeeac404565ab099c Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 9 Feb 2021 19:52:58 +0800 Subject: [PATCH 03/34] Add ethgasstation flag, manual gas, token url to gateway install script --- .../docker-commands/create-gateway.sh | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index 05b99bca00..c7835aa3c7 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -100,19 +100,37 @@ do then ETHEREUM_RPC_URL="$(echo -e "${value}" | tr -d '[:space:]')" fi - # ethergas station config + # ethereum token list source + if [ "$key" == "ethereum_token_list_url" ] + then + ETHEREUM_TOKEN_LIST_URL="$(echo -e "${value}" | tr -d '[:space:]')" + fi + # manual gas + if [ "$key" == "manual_gas_price" ] + then + MANUAL_GAS_PRICE="$(echo -e "${value}" | tr -d '[:space:]')" + fi + # enable eth gas station + if [ "$key" == "ethgasstation_gas_enabled" ] + then + ENABLE_ETH_GAS_STATION="$(echo -e "${value}" | tr -d '[:space:]')" + fi + # ethergas station api key if [ "$key" == "ethgasstation_api_key" ] then ETH_GAS_STATION_API_KEY="$(echo -e "${value}" | tr -d '[:space:]')" fi + # Gas Level (fast, fastest, safeLow) if [ "$key" == "ethgasstation_gas_level" ] then ETH_GAS_STATION_GAS_LEVEL="$(echo -e "${value}" | tr -d '[:space:]')" fi + # Refresh time in second if [ "$key" == "ethgasstation_refresh_time" ] then ETH_GAS_STATION_REFRESH_TIME="$(echo -e "${value}" | tr -d '[:space:]')" fi + # done < "$GLOBAL_CONFIG" } read_global_config @@ -231,8 +249,15 @@ 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" "Terra Chain:" "$TERRA" printf "%30s %5s\n" "Gateway Log Path:" "$LOG_PATH" @@ -249,20 +274,30 @@ 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 "ETHEREUM_CHAIN=$ETHEREUM_CHAIN" >> $ENV_FILE echo "ETHEREUM_RPC_URL=$ETHEREUM_RPC_URL" >> $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 "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE -echo "TERRA_LCD_URL=$TERRA_LCD_URL" >> $ENV_FILE -echo "TERRA_CHAIN=$TERRA_CHAIN" >> $ENV_FILE +echo "ETHEREUM_TOKEN_LIST_URL=$ETHEREUM_TOKEN_LIST_URL" >> $ENV_FILE +echo "MANUAL_GAS_PRICE=$MANUAL_GAS_PRICE" >> $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 +# balancer config +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 "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE + +# terra config +echo "TERRA_LCD_URL=$TERRA_LCD_URL" >> $ENV_FILE +echo "TERRA_CHAIN=$TERRA_CHAIN" >> $ENV_FILE + echo "" >> $ENV_FILE prompt_proceed () { @@ -270,7 +305,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 } From 57e127dab1e403d8a861427a88b9068642cf6cb2 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Fri, 19 Feb 2021 08:47:40 +0800 Subject: [PATCH 04/34] (fix) error in getting balance & allowance --- .../connector/balancer/balancer_connector.py | 16 +++++++--------- .../connector/uniswap/uniswap_connector.py | 13 +++++++------ hummingbot/core/utils/eth_gas_station_lookup.py | 4 +++- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index fff00f61b2..e17bbbea9e 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -173,8 +173,8 @@ async def get_allowances(self) -> Dict[str, Decimal]: resp = await self._api_request("post", "eth/allowances", {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", "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) @@ -196,8 +196,11 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "amount": amount, "side": side}) - if resp["price"] is not None: - return Decimal(str(resp["price"])) + if "price" not in resp.keys(): + self.logger().info(f"Unable to get price: {resp['info']}") + else: + if resp["price"] is not None: + return Decimal(str(resp["price"])) except asyncio.CancelledError: raise except Exception as e: @@ -492,9 +495,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. @@ -509,8 +509,6 @@ async def _update_balances(self, on_interval = False): "eth/balances", {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).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) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 5ee7352879..f01c079156 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -172,8 +172,8 @@ async def get_allowances(self) -> Dict[str, Decimal]: resp = await self._api_request("post", "eth/allowances", {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", "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) @@ -195,8 +195,11 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "side": side.upper(), "amount": amount}) - if resp["price"] is not None: - return Decimal(str(resp["price"])) + if "price" not in resp.keys(): + self.logger().info(f"Unable to get price: {resp['info']}") + else: + if resp["price"] is not None: + return Decimal(str(resp["price"])) except asyncio.CancelledError: raise except Exception as e: @@ -508,8 +511,6 @@ async def _update_balances(self): "eth/balances", {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).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) diff --git a/hummingbot/core/utils/eth_gas_station_lookup.py b/hummingbot/core/utils/eth_gas_station_lookup.py index 54674d54f3..35f99f586c 100644 --- a/hummingbot/core/utils/eth_gas_station_lookup.py +++ b/hummingbot/core/utils/eth_gas_station_lookup.py @@ -146,8 +146,10 @@ async def gas_price_update_loop(self): await asyncio.sleep(self.refresh_time) except asyncio.CancelledError: raise + except requests.exceptions.ConnectionError as e: + self.logger().info('Connection Error : ' + str(e)) except Exception: - self.logger().network("Unexpected error running logging task.", exc_info=True) + self.logger().network("Unexpected error in getting eth gas estimate.", exc_info=True) await asyncio.sleep(self.refresh_time) async def start_network(self): From ed7188fbbc3c6bab4cf20fe63987c601a60c93e2 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 25 Feb 2021 14:40:30 +0800 Subject: [PATCH 05/34] (feat) Update Gateway config in install script * prompt for eth rpc url * prompt for eth gas station setting * prompt for token list url overwrite --- .../docker-commands/create-gateway.sh | 218 ++++++++++++------ 1 file changed, 145 insertions(+), 73 deletions(-) diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index c7835aa3c7..c0ec6d7489 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -79,76 +79,137 @@ 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" ] - then - ETHEREUM_RPC_URL="$(echo -e "${value}" | tr -d '[:space:]')" - fi - # ethereum token list source - if [ "$key" == "ethereum_token_list_url" ] - then - ETHEREUM_TOKEN_LIST_URL="$(echo -e "${value}" | tr -d '[:space:]')" - fi - # manual gas - if [ "$key" == "manual_gas_price" ] - then - MANUAL_GAS_PRICE="$(echo -e "${value}" | tr -d '[:space:]')" - fi - # enable eth gas station - if [ "$key" == "ethgasstation_gas_enabled" ] - then - ENABLE_ETH_GAS_STATION="$(echo -e "${value}" | tr -d '[:space:]')" - fi - # ethergas station api key - if [ "$key" == "ethgasstation_api_key" ] +} +prompt_ethereum_setup + +# prompt to ethereum rpc +prompt_ethereum_rpc_setup () { + if [ "$ETHEREUM_RPC_URL" == "" ] then - ETH_GAS_STATION_API_KEY="$(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 - # Gas Level (fast, fastest, safeLow) - if [ "$key" == "ethgasstation_gas_level" ] +} +prompt_ethereum_rpc_setup + +# 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 - ETH_GAS_STATION_GAS_LEVEL="$(echo -e "${value}" | tr -d '[:space:]')" + ETHEREUM_TOKEN_LIST_URL=https://wispy-bird-88a7.uniswap.workers.dev/?url=http://tokens.1inch.eth.link fi - # Refresh time in second - if [ "$key" == "ethgasstation_refresh_time" ] +} +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_REFRESH_TIME="$(echo -e "${value}" | tr -d '[:space:]')" + 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" != "average" ]] + then + prompt_eth_gasstation_gas_level + fi fi - # -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] >>> " PROCEED - if [[ "$PROCEED" == "Y" || "$PROCEED" == "y" ]] +# 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 - echo "ℹ️ Retrieving Balancer/Uniswap config from Hummingbot config file ... " - ETHEREUM_SETUP=true - echo + 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 } -prompt_ethereum_setup +prompt_eth_gasstation_setup prompt_balancer_setup () { # Ask the user for the max balancer pool to use + echo read -p " Enter the maximum balancer swap pool (default = \"4\") >>> " BALANCER_MAX_SWAPS if [ "$BALANCER_MAX_SWAPS" == "" ] then @@ -165,31 +226,34 @@ fi # 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" ] -then - TERRA_LCD_URL="https://tequila-lcd.terra.dev" - TERRA_CHAIN="tequila-0004" -fi + 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 @@ -276,25 +340,33 @@ 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 "MANUAL_GAS_PRICE=$MANUAL_GAS_PRICE" >> $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 # 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 From f793f3432ac339b9061db861c12a484f0e17c96f Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 1 Mar 2021 20:15:28 +0800 Subject: [PATCH 06/34] (feat) Remove ETH Gas Station price lookup & config * Set default ethereum token list url using 1inch token list in global template yaml * Remove balancer max swap setting in global config * Remove ERC 20 Token Address overwrite file (now using Gateway config) * (fix) Update gas price in tracking order using Gateway's return gas value in `/trade` (uniswap, balancer) * (fix) Update gateway install script to accept lowercase param --- hummingbot/client/command/config_command.py | 7 +---- hummingbot/client/command/start_command.py | 5 ---- hummingbot/client/command/status_command.py | 6 +---- hummingbot/client/config/config_helpers.py | 15 ----------- hummingbot/client/config/global_config_map.py | 26 ------------------- hummingbot/client/settings.py | 1 - .../connector/balancer/balancer_connector.py | 20 ++++++-------- .../connector/uniswap/uniswap_connector.py | 21 +++++---------- hummingbot/core/utils/estimate_fee.py | 5 ---- hummingbot/templates/conf_global_TEMPLATE.yml | 11 ++------ .../docker-commands/create-gateway.sh | 2 +- 11 files changed, 20 insertions(+), 99 deletions(-) diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 444609ce50..00565bcce6 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -46,15 +46,10 @@ "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", - "gateway_api_port", - "balancer_max_swaps"] + "gateway_api_port"] class ConfigCommand: diff --git a/hummingbot/client/command/start_command.py b/hummingbot/client/command/start_command.py index e8c2e589a3..3c36475ee1 100644 --- a/hummingbot/client/command/start_command.py +++ b/hummingbot/client/command/start_command.py @@ -20,14 +20,12 @@ from hummingbot.client.settings import ( STRATEGIES, SCRIPTS_PATH, - ethereum_gas_station_required, required_exchanges, ) from hummingbot.core.utils.async_utils import safe_ensure_future 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 if TYPE_CHECKING: @@ -142,9 +140,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 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/config/config_helpers.py b/hummingbot/client/config/config_helpers.py index e99e893667..b2500628ab 100644 --- a/hummingbot/client/config/config_helpers.py +++ b/hummingbot/client/config/config_helpers.py @@ -31,7 +31,6 @@ CONF_FILE_PATH, CONF_POSTFIX, CONF_PREFIX, - TOKEN_ADDRESSES_FILE_PATH, CONNECTOR_SETTINGS ) from hummingbot.client.config.security import Security @@ -166,7 +165,6 @@ def get_eth_wallet_private_key() -> Optional[str]: def get_erc20_token_addresses() -> Dict[str, List]: token_list_url = global_config_map.get("ethereum_token_list_url").value - address_file_path = TOKEN_ADDRESSES_FILE_PATH token_list = {} resp = requests.get(token_list_url) @@ -175,19 +173,6 @@ def get_erc20_token_addresses() -> Dict[str, List]: for token in decoded_resp["tokens"]: token_list[token["symbol"]] = [token["address"], token["decimals"]] - try: - with open(address_file_path) as f: - overrides: Dict[str, str] = json.load(f) - for token, address in overrides.items(): - override_token = token_list.get(token, [address, 18]) - token_list[token] = [address, override_token[1]] - except FileNotFoundError: - # create override file for first run w docker - with open(address_file_path, "w+") as f: - f.write(json.dumps({})) - except Exception as e: - logging.getLogger().error(e, exc_info=True) - return token_list diff --git a/hummingbot/client/config/global_config_map.py b/hummingbot/client/config/global_config_map.py index d3702700c2..f454c66a03 100644 --- a/hummingbot/client/config/global_config_map.py +++ b/hummingbot/client/config/global_config_map.py @@ -272,32 +272,6 @@ def connector_keys(): 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/settings.py b/hummingbot/client/settings.py index ae6392fde7..799f3bb2c7 100644 --- a/hummingbot/client/settings.py +++ b/hummingbot/client/settings.py @@ -23,7 +23,6 @@ ENCYPTED_CONF_POSTFIX = ".json" GLOBAL_CONFIG_PATH = "conf/conf_global.yml" TRADE_FEES_CONFIG_PATH = "conf/conf_fee_overrides.yml" -TOKEN_ADDRESSES_FILE_PATH = "conf/erc20_tokens_override.json" DEFAULT_KEY_FILE_PATH = "conf/" DEFAULT_LOG_FILE_PATH = "logs/" DEFAULT_ETHEREUM_RPC_URL = "https://mainnet.coinalpha.com/hummingbot-test-node" diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index e17bbbea9e..b129f4d8b3 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -31,7 +31,6 @@ 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 @@ -76,7 +75,6 @@ def __init__(self, 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._wallet_private_key = wallet_private_key self._ethereum_rpc_url = ethereum_rpc_url self._trading_required = trading_required @@ -125,8 +123,7 @@ async def initiate_pool(self) -> str: base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/balancer/start", {"base": base, - "quote": quote, - "gasPrice": str(get_gas_price()) + "quote": quote }) status = resp["success"] return status @@ -155,7 +152,6 @@ async def approve_balancer_spender(self, token_symbol: str) -> Decimal: resp = await self._api_request("post", "eth/approve", {"token": token_symbol, - "gasPrice": str(get_gas_price()), "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -195,12 +191,13 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal {"base": base, "quote": quote, "amount": amount, - "side": side}) + "side": side.upper()}) if "price" not in resp.keys(): self.logger().info(f"Unable to get price: {resp['info']}") else: - if resp["price"] is not None: - return Decimal(str(resp["price"])) + if resp["price"] is not None and resp["gasPrice"] is not None: + gas_price = resp["gasPrice"] + return Decimal(str(gas_price)) except asyncio.CancelledError: raise except Exception as e: @@ -270,18 +267,17 @@ 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": base, "quote": quote, "side": trade_type.name.upper(), "amount": str(amount), "limitPrice": str(price), - "gasPrice": str(gas_price), } - self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) try: order_result = await self._api_request("post", "eth/balancer/trade", api_params) hash = order_result.get("txHash") + gas_price = order_result.get("gasPrice") + 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} " @@ -429,7 +425,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._token_addresses.keys()) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index f01c079156..e19fff207e 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -31,7 +31,6 @@ 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 @@ -76,7 +75,6 @@ def __init__(self, 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._wallet_private_key = wallet_private_key self._ethereum_rpc_url = ethereum_rpc_url self._trading_required = trading_required @@ -124,8 +122,7 @@ async def initiate_pool(self) -> str: base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/uniswap/start", {"base": base, - "quote": quote, - "gasPrice": str(get_gas_price()) + "quote": quote }) status = resp["success"] return status @@ -154,7 +151,6 @@ async def approve_uniswap_spender(self, token_symbol: str) -> Decimal: resp = await self._api_request("post", "eth/approve", {"token": token_symbol, - "gasPrice": str(get_gas_price()), "connector": self.name}) amount_approved = Decimal(str(resp["amount"])) if amount_approved > 0: @@ -198,8 +194,9 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal if "price" not in resp.keys(): self.logger().info(f"Unable to get price: {resp['info']}") else: - if resp["price"] is not None: - return Decimal(str(resp["price"])) + if resp["price"] is not None and resp["gasPrice"] is not None: + gas_price = resp["gasPrice"] + return Decimal(str(gas_price)) except asyncio.CancelledError: raise except Exception as e: @@ -269,18 +266,17 @@ 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": base, "quote": quote, "side": trade_type.name.upper(), "amount": str(amount), "limitPrice": str(price), - "gasPrice": str(gas_price), } - self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) try: order_result = await self._api_request("post", "eth/uniswap/trade", api_params) hash = order_result.get("txHash") + gas_price = order_result.get("gasPrice") + 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} " @@ -428,7 +424,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._token_addresses.keys()) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property @@ -494,9 +490,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): """ Calls Eth API to update total and available balances. 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/templates/conf_global_TEMPLATE.yml b/hummingbot/templates/conf_global_TEMPLATE.yml index 8f77ed6df7..eb2bd2ae9c 100644 --- a/hummingbot/templates/conf_global_TEMPLATE.yml +++ b/hummingbot/templates/conf_global_TEMPLATE.yml @@ -82,7 +82,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 @@ -160,14 +160,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 c0ec6d7489..1bb80a6618 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -161,7 +161,7 @@ prompt_eth_gasstation_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" != "average" ]] + 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 From 534d077d5dd0a04d2895db53bb4cb4eae03afb18 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 2 Mar 2021 18:25:54 +0800 Subject: [PATCH 07/34] Add gas info logging for created order --- hummingbot/connector/connector/balancer/balancer_connector.py | 2 +- hummingbot/connector/connector/uniswap/uniswap_connector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index b129f4d8b3..063f4b236c 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -281,7 +281,7 @@ async def _create_order(self, 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}. Gas price: {gas_price}") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price if hash is not None: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index e19fff207e..ee9a2e549c 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -280,7 +280,7 @@ async def _create_order(self, 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}. Gas price: {gas_price}") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price if hash is not None: From a08a481bc68fa560623fe34b367d1968a67222ec Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 3 Mar 2021 06:52:10 +0800 Subject: [PATCH 08/34] (feat) Add gas cost estimation from json response --- .../connector/connector/balancer/balancer_connector.py | 5 ++++- hummingbot/connector/connector/uniswap/uniswap_connector.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 063f4b236c..493b50a4c9 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -277,11 +277,14 @@ async def _create_order(self, 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) if tracked_order is not None: self.logger().info(f"Created {trade_type.name} order {order_id} txHash: {hash} " - f"for {amount} {trading_pair}. Gas price: {gas_price}") + 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: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index ee9a2e549c..041beec2fa 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -276,11 +276,14 @@ async def _create_order(self, 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}. Gas price: {gas_price}") + 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: From 5cf3eeaef270eedc8e414c9d8a5c06b2d5741c2f Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 3 Mar 2021 07:25:32 +0800 Subject: [PATCH 09/34] Change new order logging message "gas cost" to "transaction fee" --- hummingbot/connector/connector/balancer/balancer_connector.py | 2 +- hummingbot/connector/connector/uniswap/uniswap_connector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 493b50a4c9..237449933f 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -283,7 +283,7 @@ async def _create_order(self, 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}. Estimated Gas Cost: {gas_cost} ETH " + f"for {amount} {trading_pair}. Estimated Transaction Fee: {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 diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 041beec2fa..ab503df582 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -282,7 +282,7 @@ async def _create_order(self, 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}. Estimated Gas Cost: {gas_cost} ETH " + f"for {amount} {trading_pair}. Estimated Transaction Fee: {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 From 6f15ee5e06b8cb088d673083fa42ae2db93312ad Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 3 Mar 2021 17:31:42 +0800 Subject: [PATCH 10/34] (fix) price error in json response retrievel * handle status command on 'no pool' price in-availability --- hummingbot/client/command/config_command.py | 1 - .../connector/balancer/balancer_connector.py | 15 +++++++++------ .../connector/uniswap/uniswap_connector.py | 11 +++++------ hummingbot/strategy/amm_arb/amm_arb.py | 13 +++++++++---- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 00565bcce6..f71ec2a81c 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -44,7 +44,6 @@ "send_error_logs", "script_enabled", "script_file_path", - "manual_gas_price", "ethereum_chain_name", "gateway_enabled", "gateway_cert_passphrase", diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 237449933f..3acbac95f2 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -192,12 +192,11 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "amount": amount, "side": side.upper()}) - if "price" not in resp.keys(): + if "price" not in resp.keys() or "gasPrice" not in resp.keys(): self.logger().info(f"Unable to get price: {resp['info']}") else: - if resp["price"] is not None and resp["gasPrice"] is not None: - gas_price = resp["gasPrice"] - return Decimal(str(gas_price)) + if resp["price"] is not None: + return Decimal(str(resp["price"])) except asyncio.CancelledError: raise except Exception as e: @@ -278,12 +277,16 @@ async def _create_order(self, hash = order_result.get("txHash") gas_price = order_result.get("gasPrice") gas_limit = order_result.get("gasLimit") - gas_cost = order_result.get("gasCost") + transaction_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 + 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}. Estimated Transaction Fee: {gas_cost} ETH " + f"for {amount} {trading_pair}. Estimated Transaction Fee: {transaction_cost} ETH " f" (gas limit: {gas_limit}, gas price: {gas_price})") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index ab503df582..495ce243e1 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -191,12 +191,11 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "side": side.upper(), "amount": amount}) - if "price" not in resp.keys(): + if "price" not in resp.keys() or "gasPrice" not in resp.keys(): self.logger().info(f"Unable to get price: {resp['info']}") else: - if resp["price"] is not None and resp["gasPrice"] is not None: - gas_price = resp["gasPrice"] - return Decimal(str(gas_price)) + if resp["price"] is not None: + return Decimal(str(resp["price"])) except asyncio.CancelledError: raise except Exception as e: @@ -277,12 +276,12 @@ async def _create_order(self, hash = order_result.get("txHash") gas_price = order_result.get("gasPrice") gas_limit = order_result.get("gasLimit") - gas_cost = order_result.get("gasCost") + transaction_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}. Estimated Transaction Fee: {gas_cost} ETH " + f"for {amount} {trading_pair}. Estimated Transaction Fee: {transaction_cost} ETH " f" (gas limit: {gas_limit}, gas price: {gas_price})") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price diff --git a/hummingbot/strategy/amm_arb/amm_arb.py b/hummingbot/strategy/amm_arb/amm_arb.py index b96af808fc..da5adf66a4 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 = float(buy_price) if buy_price is not None else '-' + sell_price = float(sell_price) if sell_price is not None else '-' + mid_price = float((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 = [] From 7b9e1cad843188dcbe98ca1682314930a3549efa Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 4 Mar 2021 17:29:29 +0800 Subject: [PATCH 11/34] (fix) fee not added to calculation. Add fee to fee overwrite config map --- .../connector/balancer/balancer_connector.py | 24 ++++++++++++++----- .../connector/uniswap/uniswap_connector.py | 19 ++++++++++++--- hummingbot/strategy/amm_arb/amm_arb.py | 3 ++- hummingbot/strategy/amm_arb/data_types.py | 4 +++- .../templates/conf_fee_overrides_TEMPLATE.yml | 3 +++ 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 3acbac95f2..71b18d85e0 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -33,6 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH from hummingbot.client.config.global_config_map import global_config_map from hummingbot.client.config.config_helpers import get_erc20_token_addresses +from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None s_decimal_0 = Decimal("0") @@ -82,6 +83,7 @@ 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 @@ -119,7 +121,7 @@ async def initiate_pool(self) -> str: Initiate to cache pools and auto approve allowances for token in trading_pairs :return: A success/fail status for initiation """ - self.logger().info("Initializing strategy and caching swap pools ...") + self.logger().info("Initializing strategy and caching Balancer swap pools ...") base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/balancer/start", {"base": base, @@ -192,10 +194,20 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "amount": amount, "side": side.upper()}) - if "price" not in resp.keys() or "gasPrice" not in resp.keys(): - self.logger().info(f"Unable to get price: {resp['info']}") + if "price" not in resp.keys() or "gasPrice" not in resp.keys() or "gasCost" not in resp.keys(): + self.logger().info(f"Unable to get price or gas: {resp['info']}") else: if resp["price"] is not None: + # overwrite fee with gas cost (tx cost) + gas_cost = resp["gasCost"] + + if self._last_est_gas_cost_reported < self.current_timestamp - 20.: + self.logger().info(f"Estimated gas cost: {gas_cost} ETH") + self._last_est_gas_cost_reported = self.current_timestamp + + # 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(resp["price"])) except asyncio.CancelledError: raise @@ -277,16 +289,16 @@ async def _create_order(self, hash = order_result.get("txHash") gas_price = order_result.get("gasPrice") gas_limit = order_result.get("gasLimit") - transaction_cost = order_result.get("gasCost") + 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 - self._update_balances() + 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}. Estimated Transaction Fee: {transaction_cost} ETH " + 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 diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 495ce243e1..b09c14855d 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -33,6 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH from hummingbot.client.config.global_config_map import global_config_map from hummingbot.client.config.config_helpers import get_erc20_token_addresses +from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None s_decimal_0 = Decimal("0") @@ -82,6 +83,7 @@ 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 @@ -118,7 +120,7 @@ async def initiate_pool(self) -> str: Initiate to cache pools and auto approve allowances for token in trading_pairs :return: A success/fail status for initiation """ - self.logger().info("Initializing strategy and caching swap pools ...") + self.logger().info("Initializing strategy and caching Uniswap swap pools ...") base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/uniswap/start", {"base": base, @@ -195,6 +197,17 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal self.logger().info(f"Unable to get price: {resp['info']}") else: if resp["price"] is not None: + # overwrite fee with gas cost (tx cost) + gas_cost = resp["gasCost"] + + if self._last_est_gas_cost_reported < self.current_timestamp - 20.: + self.logger().info(f"Estimated gas cost: {gas_cost} ETH") + self._last_est_gas_cost_reported = self.current_timestamp + + # 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(resp["price"])) except asyncio.CancelledError: raise @@ -276,12 +289,12 @@ async def _create_order(self, hash = order_result.get("txHash") gas_price = order_result.get("gasPrice") gas_limit = order_result.get("gasLimit") - transaction_cost = order_result.get("gasCost") + 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}. Estimated Transaction Fee: {transaction_cost} ETH " + 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 diff --git a/hummingbot/strategy/amm_arb/amm_arb.py b/hummingbot/strategy/amm_arb/amm_arb.py index da5adf66a4..b544fe41d4 100644 --- a/hummingbot/strategy/amm_arb/amm_arb.py +++ b/hummingbot/strategy/amm_arb/amm_arb.py @@ -340,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: @@ -353,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/amm_arb/data_types.py b/hummingbot/strategy/amm_arb/data_types.py index 70375d4b93..93b5503c02 100644 --- a/hummingbot/strategy/amm_arb/data_types.py +++ b/hummingbot/strategy/amm_arb/data_types.py @@ -79,7 +79,9 @@ def profit_pct(self, account_for_fee: bool = False, first_side_quote_eth_rate: D sell_gained_net = (sell.amount * sell.quote_price) - sell_fee_amount buy_spent_net = (buy.amount * buy.quote_price) + buy_fee_amount - return ((sell_gained_net - buy_spent_net) / buy_spent_net) if buy_spent_net != s_decimal_0 else s_decimal_0 + profit_percentage = ((sell_gained_net - buy_spent_net) / buy_spent_net) if buy_spent_net != s_decimal_0 else s_decimal_0 + + return profit_percentage def __repr__(self): return f"First Side - {self.first_side}\nSecond Side - {self.second_side}" diff --git a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml index 506eb80b09..696a3c6c27 100644 --- a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml +++ b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml @@ -68,5 +68,8 @@ 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: From d7d637db3337c8a1d3c3ecd8db11d338d03abc13 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Fri, 5 Mar 2021 16:55:13 +0800 Subject: [PATCH 12/34] (feat) Handle insufficient ETH balance to cover estimated gas cost --- .../connector/balancer/balancer_connector.py | 15 ++++++++++----- .../connector/uniswap/uniswap_connector.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 237449933f..18c2b7f54d 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -192,12 +192,17 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "amount": amount, "side": side.upper()}) - if "price" not in resp.keys(): - self.logger().info(f"Unable to get price: {resp['info']}") + required_items = ["price", "gasLimit", "gasPrice", "gasCost"] + if any(item not in resp.keys() for item in required_items): + self.logger().info(f"Unable to get price (incomplete result): {resp['info']}") else: - if resp["price"] is not None and resp["gasPrice"] is not None: - gas_price = resp["gasPrice"] - return Decimal(str(gas_price)) + gas_cost = resp["gasCost"] + price = resp["price"] + if price is not None and self._account_balances["ETH"] > gas_cost: + return Decimal(str(price)) + else: + self.logger().info(f"Insufficient ETH Balance to cover gas:" + f" Balance: {self._account_balances['ETH']}. Est gas cost: {gas_cost}") except asyncio.CancelledError: raise except Exception as e: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index ab503df582..a546e7839f 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -191,12 +191,17 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "quote": quote, "side": side.upper(), "amount": amount}) - if "price" not in resp.keys(): - self.logger().info(f"Unable to get price: {resp['info']}") + required_items = ["price", "gasLimit", "gasPrice", "gasCost"] + if any(item not in resp.keys() for item in required_items): + self.logger().info(f"Unable to get price (incomplete result): {resp['info']}") else: - if resp["price"] is not None and resp["gasPrice"] is not None: - gas_price = resp["gasPrice"] - return Decimal(str(gas_price)) + gas_cost = resp["gasCost"] + price = resp["price"] + if price is not None and self._account_balances["ETH"] > gas_cost: + return Decimal(str(price)) + else: + self.logger().info(f"Insufficient ETH Balance to cover gas:" + f" Balance: {self._account_balances['ETH']}. Est gas cost: {gas_cost}") except asyncio.CancelledError: raise except Exception as e: From e50008933a33fa6055650194cb7b56fb46eba50a Mon Sep 17 00:00:00 2001 From: sdgoh Date: Mon, 8 Mar 2021 20:56:05 +0800 Subject: [PATCH 13/34] (feat) Check for exceptions in balancer, gas, no token contract address --- .../connector/balancer/balancer_connector.py | 57 +++++++++++++----- .../connector/uniswap/uniswap_connector.py | 59 ++++++++++++++----- hummingbot/core/utils/ethereum.py | 40 +++++++++++++ 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 18c2b7f54d..277ada19d9 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -33,6 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH 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_execptions s_logger = None s_decimal_0 = Decimal("0") @@ -87,6 +88,7 @@ def __init__(self, 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 @@ -116,17 +118,26 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate to cache pools and auto approve allowances for token in trading_pairs - :return: A success/fail status for initiation + Initiate to cache swap pools for token in trading_pairs """ - self.logger().info("Initializing strategy and caching swap pools ...") - base, quote = self._trading_pairs[0].split("-") - resp = await self._api_request("post", "eth/balancer/start", - {"base": base, - "quote": quote - }) - status = resp["success"] - return status + try: + self.logger().info(f"Initializing strategy and caching Balancer {self._trading_pairs[0]} swap pools ...") + base, quote = self._trading_pairs[0].split("-") + resp = await self._api_request("post", "eth/balancer/start", + {"base": base, + "quote": quote + }) + 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): """ @@ -194,15 +205,33 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "side": side.upper()}) required_items = ["price", "gasLimit", "gasPrice", "gasCost"] if any(item not in resp.keys() for item in required_items): - self.logger().info(f"Unable to get price (incomplete result): {resp['info']}") + self.logger().info(f"Unable to get price: {resp['info']}") else: + + gas_limit = resp["gasLimit"] + gas_price = resp["gasPrice"] gas_cost = resp["gasCost"] price = resp["price"] - if price is not None and self._account_balances["ETH"] > gas_cost: + 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_execptions(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: return Decimal(str(price)) else: - self.logger().info(f"Insufficient ETH Balance to cover gas:" - f" Balance: {self._account_balances['ETH']}. Est gas cost: {gas_cost}") + self.logger().info(f"Error getting quote price from result: {resp['info']}") except asyncio.CancelledError: raise except Exception as e: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index a546e7839f..a0f1001f90 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -33,6 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH 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_execptions s_logger = None s_decimal_0 = Decimal("0") @@ -87,6 +88,7 @@ def __init__(self, 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 @@ -115,17 +117,26 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate to cache pools and auto approve allowances for token in trading_pairs - :return: A success/fail status for initiation + Initiate strategy. Skip initializing pool """ - self.logger().info("Initializing strategy and caching swap pools ...") - base, quote = self._trading_pairs[0].split("-") - resp = await self._api_request("post", "eth/uniswap/start", - {"base": base, - "quote": quote - }) - status = resp["success"] - return status + try: + self.logger().info("Initializing Uniswap") + base, quote = self._trading_pairs[0].split("-") + resp = await self._api_request("post", "eth/uniswap/start", + {"base": base, + "quote": quote + }) + 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): """ @@ -193,15 +204,35 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "amount": amount}) required_items = ["price", "gasLimit", "gasPrice", "gasCost"] if any(item not in resp.keys() for item in required_items): - self.logger().info(f"Unable to get price (incomplete result): {resp['info']}") + 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: {resp}") else: + gas_limit = resp["gasLimit"] + gas_price = resp["gasPrice"] gas_cost = resp["gasCost"] price = resp["price"] - if price is not None and self._account_balances["ETH"] > gas_cost: + 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_execptions(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: return Decimal(str(price)) else: - self.logger().info(f"Insufficient ETH Balance to cover gas:" - f" Balance: {self._account_balances['ETH']}. Est gas cost: {gas_cost}") + self.logger().info(f"Error getting quote price from result: {resp['info']}") except asyncio.CancelledError: raise except Exception as e: diff --git a/hummingbot/core/utils/ethereum.py b/hummingbot/core/utils/ethereum.py index ff667d3a0a..44ffc2bc8d 100644 --- a/hummingbot/core/utils/ethereum.py +++ b/hummingbot/core/utils/ethereum.py @@ -32,3 +32,43 @@ def block_values_to_hex(block: AttributeDict) -> AttributeDict: except binascii.Error: formatted_block[key] = value return AttributeDict(formatted_block) + + +def check_transaction_execptions(trade_data: dict) -> dict: + + exception_list = [] + + # gas_limit = trade_data["gas_limit"] + # gas_price = trade_data["gas_price"] + gas_cost = trade_data["gas_cost"] + # price = trade_data["price"] + amount = trade_data["amount"] + side = trade_data["side"] + base = trade_data["base"] + quote = trade_data["quote"] + balances = trade_data["balances"] + allowances = trade_data["allowances"] + + eth_balance = balances["ETH"] + # base_balance = balances[base] + # quote_balance = balances[quote] + + # 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}") + + trade_token = base if side == "side" else quote + trade_balance = balances[trade_token] + trade_allowance = allowances[trade_token] + + # check for insufficient balance + if trade_balance < amount: + exception_list.append(f"Insufficient ETH balance to {side}:" + f" Balance: {trade_balance}. Amount to trade: {amount}") + + # 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 From 20214c21d95d9e73b5ef7400c97c10a38d620ef6 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Thu, 11 Mar 2021 03:14:27 +0800 Subject: [PATCH 14/34] (feat) Add minimum recommended gas limit theshold * fix incorrect class name spelling --- .../connector/balancer/balancer_connector.py | 15 ++++++++------- .../connector/uniswap/uniswap_connector.py | 10 ++++------ hummingbot/core/utils/ethereum.py | 19 ++++++++----------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 277ada19d9..22e0e01c54 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -33,7 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH 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_execptions +from hummingbot.core.utils.ethereum import check_transaction_exceptions s_logger = None s_decimal_0 = Decimal("0") @@ -205,9 +205,11 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "side": side.upper()}) required_items = ["price", "gasLimit", "gasPrice", "gasCost"] if any(item not in resp.keys() for item in required_items): - self.logger().info(f"Unable to get price: {resp['info']}") + 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"] @@ -222,16 +224,15 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "gas_limit": gas_limit, "gas_price": gas_price, "gas_cost": gas_cost, - "price": price + "price": price, + "swaps": len(resp["swaps"]) } - exceptions = check_transaction_execptions(account_standing) + 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: return Decimal(str(price)) - else: - self.logger().info(f"Error getting quote price from result: {resp['info']}") except asyncio.CancelledError: raise except Exception as e: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index a0f1001f90..1b51bfce24 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -33,7 +33,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH 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_execptions +from hummingbot.core.utils.ethereum import check_transaction_exceptions s_logger = None s_decimal_0 = Decimal("0") @@ -205,9 +205,9 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal 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']}") + self.logger().info(f"Unable to get price. {resp['info']}") else: - self.logger().info(f"Missing data from price result: {resp}") + self.logger().info(f"Missing data from price result. Incomplete return result for ({resp.keys()})") else: gas_limit = resp["gasLimit"] gas_price = resp["gasPrice"] @@ -225,14 +225,12 @@ async def get_quote_price(self, trading_pair: str, is_buy: bool, amount: Decimal "gas_cost": gas_cost, "price": price } - exceptions = check_transaction_execptions(account_standing) + 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: return Decimal(str(price)) - else: - self.logger().info(f"Error getting quote price from result: {resp['info']}") except asyncio.CancelledError: raise except Exception as e: diff --git a/hummingbot/core/utils/ethereum.py b/hummingbot/core/utils/ethereum.py index 44ffc2bc8d..503e0c8f59 100644 --- a/hummingbot/core/utils/ethereum.py +++ b/hummingbot/core/utils/ethereum.py @@ -34,38 +34,35 @@ def block_values_to_hex(block: AttributeDict) -> AttributeDict: return AttributeDict(formatted_block) -def check_transaction_execptions(trade_data: dict) -> dict: +def check_transaction_exceptions(trade_data: dict) -> dict: exception_list = [] - # gas_limit = trade_data["gas_limit"] + gas_limit = trade_data["gas_limit"] # gas_price = trade_data["gas_price"] gas_cost = trade_data["gas_cost"] - # price = trade_data["price"] 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"] - # base_balance = balances[base] - # quote_balance = balances[quote] # 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}") + f" Balance: {eth_balance}. Est. gas cost: {gas_cost}. {swaps_message}") trade_token = base if side == "side" else quote - trade_balance = balances[trade_token] trade_allowance = allowances[trade_token] - # check for insufficient balance - if trade_balance < amount: - exception_list.append(f"Insufficient ETH balance to {side}:" - f" Balance: {trade_balance}. Amount to trade: {amount}") + # 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: From 50df0e5cacbb69cbb6f0b7e3cc02379a6b433ecd Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Fri, 12 Mar 2021 14:09:31 +0800 Subject: [PATCH 15/34] (refactor) refactor uniswap and balancer --- .../connector/balancer/balancer_connector.py | 27 +++++++++---------- .../connector/uniswap/uniswap_connector.py | 27 +++++++++---------- .../balancer/test_balancer_connector.py | 19 ++++++++----- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 71b18d85e0..7429c63e32 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -32,7 +32,6 @@ 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.client.config.global_config_map import global_config_map -from hummingbot.client.config.config_helpers import get_erc20_token_addresses from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None @@ -71,11 +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._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 @@ -97,15 +94,17 @@ 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() + 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(token_list.keys(), 2): + for base, quote in it.permutations(tokens, 2): trading_pairs.append(f"{base}-{quote}") return trading_pairs @@ -169,7 +168,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", + {"tokenList": "[" + (",".join(self._tokens)) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -443,7 +442,7 @@ def has_allowances(self) -> bool: """ Checks if all tokens have allowance (an amount approved) """ - return len(self._allowances.values()) == len(self._token_addresses.keys()) and \ + return len(self._allowances.values()) == len(self._tokens) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property @@ -521,7 +520,7 @@ async def _update_balances(self, on_interval = False): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]"}) + {"tokenList": "[" + (",".join(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)) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index b09c14855d..f0c924fca1 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -32,7 +32,6 @@ 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.client.config.global_config_map import global_config_map -from hummingbot.client.config.config_helpers import get_erc20_token_addresses from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map s_logger = None @@ -71,11 +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._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 @@ -96,15 +93,17 @@ 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() + 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(token_list.keys(), 2): + for base, quote in it.permutations(tokens, 2): trading_pairs.append(f"{base}-{quote}") return trading_pairs @@ -168,7 +167,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]", + {"tokenList": "[" + (",".join(self._tokens)) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -439,7 +438,7 @@ def has_allowances(self) -> bool: """ Checks if all tokens have allowance (an amount approved) """ - return len(self._allowances.values()) == len(self._token_addresses.keys()) and \ + return len(self._allowances.values()) == len(self._tokens) and \ all(amount > s_decimal_0 for amount in self._allowances.values()) @property @@ -517,7 +516,7 @@ async def _update_balances(self): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + ("".join(['"' + tok + '"' + "," for tok in self._token_addresses.keys()])).rstrip(",") + "]"}) + {"tokenList": "[" + ("".join(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)) diff --git a/test/connector/connector/balancer/test_balancer_connector.py b/test/connector/connector/balancer/test_balancer_connector.py index 151d0e403e..12bb213454 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" @@ -53,10 +52,10 @@ class BalancerConnectorUnitTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls._gas_price_patcher = unittest.mock.patch( - "hummingbot.connector.connector.balancer.balancer_connector.get_gas_price") - cls._gas_price_mock = cls._gas_price_patcher.start() - cls._gas_price_mock.return_value = 50 + # cls._gas_price_patcher = unittest.mock.patch( + # "hummingbot.connector.connector.balancer.balancer_connector.get_gas_price") + # cls._gas_price_mock = cls._gas_price_patcher.start() + # cls._gas_price_mock.return_value = 50 cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: BalancerConnector = BalancerConnector( @@ -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(): From 86cb505a3f7c8db897e3017c123f7f85b318c465 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Fri, 12 Mar 2021 17:16:34 +0800 Subject: [PATCH 16/34] (refactor) remove token_address list --- .../connector/uniswap/uniswap_connector.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index f0c924fca1..f795bb7ef6 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 @@ -33,6 +32,7 @@ from hummingbot.client.settings import GATEAWAY_CA_CERT_PATH, GATEAWAY_CLIENT_CERT_PATH, GATEAWAY_CLIENT_KEY_PATH from hummingbot.client.config.global_config_map import global_config_map from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map +from hummingbot.connector.connector.balancer.balancer_connector import BalancerConnector s_logger = None s_decimal_0 = Decimal("0") @@ -95,17 +95,7 @@ def name(self): @staticmethod 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 + return await BalancerConnector.fetch_trading_pairs() @property def limit_orders(self) -> List[LimitOrder]: From cb21389f0dbaccaf56a658cb96ae1b2670dfe818 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Fri, 12 Mar 2021 18:09:34 +0800 Subject: [PATCH 17/34] (refactor) move fetch trading pairs to ethereum utils --- .../connector/balancer/balancer_connector.py | 15 ++----------- .../connector/uniswap/uniswap_connector.py | 5 ++--- hummingbot/core/utils/ethereum.py | 21 ++++++++++++++++++- .../balancer/test_balancer_connector.py | 8 +++---- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index ad1b2c1967..92a4fa14eb 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 @@ -32,7 +31,7 @@ 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.client.config.global_config_map import global_config_map -from hummingbot.core.utils.ethereum import check_transaction_exceptions +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 @@ -98,17 +97,7 @@ def name(self): @staticmethod 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 + return await fetch_trading_pairs() @property def limit_orders(self) -> List[LimitOrder]: diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 3f2776900b..9f36dac749 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -31,9 +31,8 @@ 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.client.config.global_config_map import global_config_map -from hummingbot.core.utils.ethereum import check_transaction_exceptions +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 -from hummingbot.connector.connector.balancer.balancer_connector import BalancerConnector s_logger = None s_decimal_0 = Decimal("0") @@ -97,7 +96,7 @@ def name(self): @staticmethod async def fetch_trading_pairs() -> List[str]: - return await BalancerConnector.fetch_trading_pairs() + return await fetch_trading_pairs() @property def limit_orders(self) -> List[LimitOrder]: diff --git a/hummingbot/core/utils/ethereum.py b/hummingbot/core/utils/ethereum.py index 503e0c8f59..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: @@ -69,3 +73,18 @@ def check_transaction_exceptions(trade_data: dict) -> dict: 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/test/connector/connector/balancer/test_balancer_connector.py b/test/connector/connector/balancer/test_balancer_connector.py index 12bb213454..66ad53df97 100644 --- a/test/connector/connector/balancer/test_balancer_connector.py +++ b/test/connector/connector/balancer/test_balancer_connector.py @@ -52,10 +52,10 @@ class BalancerConnectorUnitTest(unittest.TestCase): @classmethod def setUpClass(cls): - # cls._gas_price_patcher = unittest.mock.patch( - # "hummingbot.connector.connector.balancer.balancer_connector.get_gas_price") - # cls._gas_price_mock = cls._gas_price_patcher.start() - # cls._gas_price_mock.return_value = 50 + cls._gas_price_patcher = unittest.mock.patch( + "hummingbot.connector.connector.balancer.balancer_connector.get_gas_price") + cls._gas_price_mock = cls._gas_price_patcher.start() + cls._gas_price_mock.return_value = 50 cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: BalancerConnector = BalancerConnector( From a68a6527192a025ca2b58c01687e791691d2006f Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Fri, 12 Mar 2021 18:14:54 +0800 Subject: [PATCH 18/34] add single quotes --- hummingbot/connector/connector/balancer/balancer_connector.py | 4 ++-- hummingbot/connector/connector/uniswap/uniswap_connector.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 92a4fa14eb..43d62ba0dd 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -168,7 +168,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + (",".join(self._tokens)) + "]", + {"tokenList": "[" + (",".join(["'" + t + "'" for t in self._tokens])) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -538,7 +538,7 @@ async def _update_balances(self, on_interval = False): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + (",".join(self._tokens)) + "]"}) + {"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)) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 9f36dac749..d39c47ca05 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -167,7 +167,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + (",".join(self._tokens)) + "]", + {"tokenList": "[" + (",".join(["'" + t + "'" for t in self._tokens])) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -532,7 +532,7 @@ async def _update_balances(self): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + ("".join(self._tokens)) + "]"}) + {"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)) From cc4f80295bf098160e351389989492bd650d8074 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 13 Mar 2021 14:14:11 +0800 Subject: [PATCH 19/34] (fix) Change single quote to double for json pair --- hummingbot/connector/connector/balancer/balancer_connector.py | 4 ++-- hummingbot/connector/connector/uniswap/uniswap_connector.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 43d62ba0dd..9bc18700a4 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -168,7 +168,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + (",".join(["'" + t + "'" for t in self._tokens])) + "]", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -538,7 +538,7 @@ async def _update_balances(self, on_interval = False): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + (",".join(["'" + t + "'" for t in self._tokens])) + "]"}) + {"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)) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index d39c47ca05..4dae517112 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -167,7 +167,7 @@ async def get_allowances(self) -> Dict[str, Decimal]: """ ret_val = {} resp = await self._api_request("post", "eth/allowances", - {"tokenList": "[" + (",".join(["'" + t + "'" for t in self._tokens])) + "]", + {"tokenList": "[" + (",".join(['"' + t + '"' for t in self._tokens])) + "]", "connector": self.name}) for token, amount in resp["approvals"].items(): ret_val[token] = Decimal(str(amount)) @@ -532,7 +532,7 @@ async def _update_balances(self): remote_asset_names = set() resp_json = await self._api_request("post", "eth/balances", - {"tokenList": "[" + ("".join(["'" + t + "'" for t in self._tokens])) + "]"}) + {"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)) From b09e00bea63112cb8e0b6dad66307de33664958a Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sat, 13 Mar 2021 15:31:41 +0800 Subject: [PATCH 20/34] (fix) Uniswap balance not updating for market ready to trade --- .../connector/balancer/balancer_connector.py | 1 + .../connector/uniswap/uniswap_connector.py | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 9bc18700a4..20536baafd 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -539,6 +539,7 @@ async def _update_balances(self, on_interval = False): 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)) diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 4dae517112..13cdeb988c 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -507,7 +507,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 @@ -520,31 +520,32 @@ async def _status_polling_loop(self): app_warning_msg="Could not fetch balances from Gateway API.") await asyncio.sleep(0.5) - 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", - {"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 + 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: """ From eb0ac485d7a02d3e87dfa31051418f95d4a5c31f Mon Sep 17 00:00:00 2001 From: sdgoh Date: Sun, 14 Mar 2021 23:45:16 +0800 Subject: [PATCH 21/34] Minor logging message update --- .../connector/connector/balancer/balancer_connector.py | 6 +++--- hummingbot/connector/connector/uniswap/uniswap_connector.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 20536baafd..ebfc26a51d 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -108,10 +108,10 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate to cache swap pools for token in trading_pairs + Initiate strategy & auto-approve allowances for trading_pairs """ try: - self.logger().info(f"Initializing strategy and caching Balancer {self._trading_pairs[0]} swap pools ...") + self.logger().info(f"Initializing Balancer {self._trading_pairs[0]} strategy & auto-approved allowances") base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/balancer/start", {"base": base, @@ -597,7 +597,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/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 13cdeb988c..fff3ff197c 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -107,10 +107,10 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate strategy. Skip initializing pool + Initiate strategy & auto-approve allowances for trading_pairs """ try: - self.logger().info("Initializing Uniswap") + self.logger().info(f"Initializing Uniswap {self._trading_pairs[0]} strategy & auto-approved allowances") base, quote = self._trading_pairs[0].split("-") resp = await self._api_request("post", "eth/uniswap/start", {"base": base, @@ -591,7 +591,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 From 07885671f9e5af102f9df15d7e66e777b2c99629 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Mon, 15 Mar 2021 11:25:53 +0800 Subject: [PATCH 22/34] (feat) removed EthGasStation and get_erc20_token_address as they used blocking requests module --- hummingbot/client/command/stop_command.py | 4 - hummingbot/client/config/config_helpers.py | 14 -- hummingbot/client/hummingbot_application.py | 9 +- .../core/utils/eth_gas_station_lookup.py | 180 ------------------ 4 files changed, 6 insertions(+), 201 deletions(-) delete mode 100644 hummingbot/core/utils/eth_gas_station_lookup.py diff --git a/hummingbot/client/command/stop_command.py b/hummingbot/client/command/stop_command.py index 3848aadea4..6eac19b03e 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 if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -45,9 +44,6 @@ async def stop_loop(self, # type: HummingbotApplication if self.strategy_task is not None and not self.strategy_task.cancelled(): self.strategy_task.cancel() - 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/config_helpers.py b/hummingbot/client/config/config_helpers.py index 40e561135a..03fb80cdc6 100644 --- a/hummingbot/client/config/config_helpers.py +++ b/hummingbot/client/config/config_helpers.py @@ -10,7 +10,6 @@ ) from collections import OrderedDict import json -import requests from typing import ( Any, Callable, @@ -163,19 +162,6 @@ def get_eth_wallet_private_key() -> Optional[str]: return account.privateKey.hex() -def get_erc20_token_addresses() -> Dict[str, List]: - token_list_url = global_config_map.get("ethereum_token_list_url").value - token_list = {} - - resp = requests.get(token_list_url, timeout=3) - decoded_resp = resp.json() - - for token in decoded_resp["tokens"]: - token_list[token["symbol"]] = [token["address"], token["decimals"]] - - return token_list - - def _merge_dicts(*args: Dict[str, ConfigVar]) -> OrderedDict: """ Helper function to merge a few dictionaries into an ordered dictionary. diff --git a/hummingbot/client/hummingbot_application.py b/hummingbot/client/hummingbot_application.py index 3566917232..2b81c62861 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, @@ -195,10 +194,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/core/utils/eth_gas_station_lookup.py b/hummingbot/core/utils/eth_gas_station_lookup.py deleted file mode 100644 index 35f99f586c..0000000000 --- a/hummingbot/core/utils/eth_gas_station_lookup.py +++ /dev/null @@ -1,180 +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}/eth/{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 requests.exceptions.ConnectionError as e: - self.logger().info('Connection Error : ' + str(e)) - except Exception: - self.logger().network("Unexpected error in getting eth gas estimate.", 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) From f2fe1a70db4b444cc58c5dcfe09a5b18a5ff16d9 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 17 Mar 2021 23:00:59 +0100 Subject: [PATCH 23/34] (refactor) modify initiate pool task to only cache pool/path and not approve tokens for spenders --- .../connector/balancer/balancer_connector.py | 11 ++++------- .../connector/connector/uniswap/uniswap_connector.py | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index ebfc26a51d..7a672a686e 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -108,15 +108,12 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate strategy & auto-approve allowances for trading_pairs + Initiate connector and cache pools """ try: - self.logger().info(f"Initializing Balancer {self._trading_pairs[0]} strategy & auto-approved allowances") - base, quote = self._trading_pairs[0].split("-") - resp = await self._api_request("post", "eth/balancer/start", - {"base": base, - "quote": quote - }) + self.logger().info(f"Initializing Balancer connector and caching pools for {self._trading_pairs}.") + resp = await self._api_request("get", "eth/balancer/start", + {"pairs": str(self._trading_pairs)}) status = bool(str(resp["success"])) if bool(str(resp["success"])): self._initiate_pool_status = status diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index fff3ff197c..2673bc3c79 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -107,15 +107,12 @@ def limit_orders(self) -> List[LimitOrder]: async def initiate_pool(self) -> str: """ - Initiate strategy & auto-approve allowances for trading_pairs + Initiate connector and start caching paths for trading_pairs """ try: - self.logger().info(f"Initializing Uniswap {self._trading_pairs[0]} strategy & auto-approved allowances") - base, quote = self._trading_pairs[0].split("-") - resp = await self._api_request("post", "eth/uniswap/start", - {"base": base, - "quote": quote - }) + self.logger().info(f"Initializing Uniswap connector and paths for {self._trading_pairs} pairs.") + resp = await self._api_request("get", "eth/uniswap/start", + {"pairs": str(self._trading_pairs)}) status = bool(str(resp["success"])) if bool(str(resp["success"])): self._initiate_pool_status = status From 8014fe8e4801844d451c4e9e729b86979bf64463 Mon Sep 17 00:00:00 2001 From: vic-en Date: Thu, 18 Mar 2021 13:46:58 +0100 Subject: [PATCH 24/34] (fix) dump list of pairs to gateway using json --- hummingbot/connector/connector/balancer/balancer_connector.py | 2 +- hummingbot/connector/connector/uniswap/uniswap_connector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/connector/balancer/balancer_connector.py b/hummingbot/connector/connector/balancer/balancer_connector.py index 7a672a686e..179c6b2e8f 100644 --- a/hummingbot/connector/connector/balancer/balancer_connector.py +++ b/hummingbot/connector/connector/balancer/balancer_connector.py @@ -113,7 +113,7 @@ async def initiate_pool(self) -> str: 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": str(self._trading_pairs)}) + {"pairs": json.dumps(self._trading_pairs)}) status = bool(str(resp["success"])) if bool(str(resp["success"])): self._initiate_pool_status = status diff --git a/hummingbot/connector/connector/uniswap/uniswap_connector.py b/hummingbot/connector/connector/uniswap/uniswap_connector.py index 2673bc3c79..c8277b65b1 100644 --- a/hummingbot/connector/connector/uniswap/uniswap_connector.py +++ b/hummingbot/connector/connector/uniswap/uniswap_connector.py @@ -112,7 +112,7 @@ async def initiate_pool(self) -> str: 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": str(self._trading_pairs)}) + {"pairs": json.dumps(self._trading_pairs)}) status = bool(str(resp["success"])) if bool(str(resp["success"])): self._initiate_pool_status = status From 6afb10f7bbf65fddd97d7075c3c75500a36a43f4 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 24 Mar 2021 19:23:10 +0800 Subject: [PATCH 25/34] (feat) Update gateway docker install script --- installation/docker-commands/create-gateway.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index e0b9dbbdc8..f77de83270 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -365,6 +365,9 @@ echo "BALANCER_MAX_SWAPS=$BALANCER_MAX_SWAPS" >> $ENV_FILE echo "" >> $ENV_FILE echo "# Uniswap Settings" >> $ENV_FILE echo "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE +echo "UNISWAP_ALLOWED_SLIPPAGE=1" >> $ENV_FILE +echo "UNISWAP_NO_RESERVE_CHECK_INTERVAL=300000" >> $ENV_FILE +echo "UNISWAP_PAIRS_CACHE_TIME=1000" >> $ENV_FILE # terra config echo "" >> $ENV_FILE @@ -372,6 +375,11 @@ 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=https://rpc.xdaichain.com" >> $ENV_FILE + echo "" >> $ENV_FILE prompt_proceed () { From c1525b587210e2854eef826bd0fd5fe51998d018 Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Sun, 28 Mar 2021 23:32:02 +0800 Subject: [PATCH 26/34] (refactor) rename Bitmax -> AscendEx --- README.md | 2 +- assets/ascend_ex_logo.png | Bin 0 -> 8570 bytes assets/bitmax_logo.png | Bin 28370 -> 0 bytes hummingbot/connector/connector_status.py | 2 +- .../{bitmax => ascend_ex}/__init__.py | 0 .../ascend_ex_active_order_tracker.pxd} | 2 +- .../ascend_ex_active_order_tracker.pyx} | 12 +- .../ascend_ex_api_order_book_data_source.py} | 20 +-- .../ascend_ex_api_user_stream_data_source.py} | 22 +-- .../ascend_ex_auth.py} | 10 +- .../exchange/ascend_ex/ascend_ex_constants.py | 7 + .../ascend_ex_exchange.py} | 132 +++++++++--------- .../ascend_ex_in_flight_order.py} | 4 +- .../ascend_ex_order_book.py} | 35 ++--- .../ascend_ex_order_book_message.py} | 4 +- .../ascend_ex_order_book_tracker.py} | 37 ++--- .../ascend_ex_order_book_tracker_entry.py | 21 +++ .../ascend_ex_user_stream_tracker.py} | 30 ++-- .../ascend_ex_utils.py} | 24 ++-- .../exchange/bitmax/bitmax_constants.py | 15 -- .../bitmax/bitmax_order_book_tracker_entry.py | 21 --- .../templates/conf_fee_overrides_TEMPLATE.yml | 4 +- hummingbot/templates/conf_global_TEMPLATE.yml | 4 +- setup.py | 2 +- .../exchange/{bitmax => ascend_ex}/.gitignore | 0 .../{bitmax => ascend_ex}/__init__.py | 0 .../test_ascend_ex_auth.py} | 15 +- .../test_ascend_ex_exchange.py} | 34 ++--- .../test_ascend_ex_order_book_tracker.py} | 12 +- .../test_ascend_ex_user_stream_tracker.py} | 16 +-- 30 files changed, 248 insertions(+), 239 deletions(-) create mode 100644 assets/ascend_ex_logo.png delete mode 100644 assets/bitmax_logo.png rename hummingbot/connector/exchange/{bitmax => ascend_ex}/__init__.py (100%) rename hummingbot/connector/exchange/{bitmax/bitmax_active_order_tracker.pxd => ascend_ex/ascend_ex_active_order_tracker.pxd} (90%) rename hummingbot/connector/exchange/{bitmax/bitmax_active_order_tracker.pyx => ascend_ex/ascend_ex_active_order_tracker.pyx} (93%) rename hummingbot/connector/exchange/{bitmax/bitmax_api_order_book_data_source.py => ascend_ex/ascend_ex_api_order_book_data_source.py} (91%) rename hummingbot/connector/exchange/{bitmax/bitmax_api_user_stream_data_source.py => ascend_ex/ascend_ex_api_user_stream_data_source.py} (83%) rename hummingbot/connector/exchange/{bitmax/bitmax_auth.py => ascend_ex/ascend_ex_auth.py} (79%) create mode 100644 hummingbot/connector/exchange/ascend_ex/ascend_ex_constants.py rename hummingbot/connector/exchange/{bitmax/bitmax_exchange.py => ascend_ex/ascend_ex_exchange.py} (88%) rename hummingbot/connector/exchange/{bitmax/bitmax_in_flight_order.py => ascend_ex/ascend_ex_in_flight_order.py} (95%) rename hummingbot/connector/exchange/{bitmax/bitmax_order_book.py => ascend_ex/ascend_ex_order_book.py} (83%) rename hummingbot/connector/exchange/{bitmax/bitmax_order_book_message.py => ascend_ex/ascend_ex_order_book_message.py} (95%) rename hummingbot/connector/exchange/{bitmax/bitmax_order_book_tracker.py => ascend_ex/ascend_ex_order_book_tracker.py} (75%) create mode 100644 hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker_entry.py rename hummingbot/connector/exchange/{bitmax/bitmax_user_stream_tracker.py => ascend_ex/ascend_ex_user_stream_tracker.py} (69%) rename hummingbot/connector/exchange/{bitmax/bitmax_utils.py => ascend_ex/ascend_ex_utils.py} (77%) delete mode 100644 hummingbot/connector/exchange/bitmax/bitmax_constants.py delete mode 100644 hummingbot/connector/exchange/bitmax/bitmax_order_book_tracker_entry.py rename test/connector/exchange/{bitmax => ascend_ex}/.gitignore (100%) rename test/connector/exchange/{bitmax => ascend_ex}/__init__.py (100%) rename test/connector/exchange/{bitmax/test_bitmax_auth.py => ascend_ex/test_ascend_ex_auth.py} (72%) rename test/connector/exchange/{bitmax/test_bitmax_exchange.py => ascend_ex/test_ascend_ex_exchange.py} (97%) rename test/connector/exchange/{bitmax/test_bitmax_order_book_tracker.py => ascend_ex/test_ascend_ex_order_book_tracker.py} (88%) rename test/connector/exchange/{bitmax/test_bitmax_user_stream_tracker.py => ascend_ex/test_ascend_ex_user_stream_tracker.py} (58%) 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 0000000000000000000000000000000000000000..23758c1611a603867832b71f5bd84e7565d7c41d GIT binary patch literal 8570 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 literal 0 HcmV?d00001 diff --git a/assets/bitmax_logo.png b/assets/bitmax_logo.png deleted file mode 100644 index 362daeca21783d84f7e93ff899b5d5302a9eccdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28370 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 bIJXrY`DW-;c?tej9YRY@PqkXvI_&=el0^x> 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 83% 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..df0f53260a 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,11 @@ 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, getWsUrlPriv, PONG_PAYLOAD -class BitmaxAPIUserStreamDataSource(UserStreamTrackerDataSource): +class AscendExAPIUserStreamDataSource(UserStreamTrackerDataSource): MAX_RETRIES = 20 MESSAGE_TIMEOUT = 10.0 PING_TIMEOUT = 5.0 @@ -26,8 +26,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,12 +50,12 @@ 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" @@ -75,12 +75,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 +89,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 4606157dd3..25d7afd816 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) + [exchange_order_id, timestamp] = ascend_ex_utils.gen_exchange_order_id(self._account_uid) 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", @@ -540,7 +540,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 +560,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 +596,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 +615,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 +639,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 +649,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 +687,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 +738,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 +783,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 +798,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 +817,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 +831,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 +869,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 +882,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 +917,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 +965,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..afe44c3101 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.ascende_ex.ascende_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.ascende_ex.ascende_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 45f943688d..1ecf284df7 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 = "hbot-" +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/templates/conf_fee_overrides_TEMPLATE.yml b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml index ed08e2fa90..08dea03918 100644 --- a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml +++ b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml @@ -72,8 +72,8 @@ okex_taker_fee: balancer_maker_fee_amount: balancer_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 2d3af846f3..df0dad24d2 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 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/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): From 1e80b064bcde3efe57de4e0772d37278ee9c7aa2 Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Mon, 29 Mar 2021 08:58:16 +0800 Subject: [PATCH 27/34] (fix) fix some invalid imports --- .../connector/exchange/ascend_ex/ascend_ex_order_book.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py index afe44c3101..88ebe8b0d1 100644 --- a/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import logging -import hummingbot.connector.exchange.ascende_ex.ascende_ex_constants as constants +import hummingbot.connector.exchange.ascend_ex.ascend_ex_constants as constants from sqlalchemy.engine import RowProxy from typing import ( @@ -13,7 +13,7 @@ from hummingbot.core.data_type.order_book_message import ( OrderBookMessage, OrderBookMessageType ) -from hummingbot.connector.exchange.ascende_ex.ascende_ex_order_book_message import AscendExOrderBookMessage +from hummingbot.connector.exchange.ascend_ex.ascend_ex_order_book_message import AscendExOrderBookMessage from hummingbot.logger import HummingbotLogger _logger = None From f9608b774f048571fb03cf722e9fe762283a9c8c Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Mon, 29 Mar 2021 11:53:20 +0800 Subject: [PATCH 28/34] (fix) ImportError in ascend_ex_api_user_stream_data_source --- .../ascend_ex/ascend_ex_api_user_stream_data_source.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py index df0f53260a..19571d90c8 100755 --- a/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_api_user_stream_data_source.py @@ -10,7 +10,8 @@ from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.logger import HummingbotLogger from hummingbot.connector.exchange.ascend_ex.ascend_ex_auth import AscendExAuth -from hummingbot.connector.exchange.ascend_ex.ascend_ex_constants import REST_URL, getWsUrlPriv, PONG_PAYLOAD +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 AscendExAPIUserStreamDataSource(UserStreamTrackerDataSource): @@ -61,7 +62,7 @@ async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: a "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)) From 8230512dc40bd8a24773ababa3dd27369e19f309 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Tue, 30 Mar 2021 19:44:17 +0800 Subject: [PATCH 29/34] (feat) Add Uniswap slippage config to Gateway docker install script --- .../docker-commands/create-gateway.sh | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index f77de83270..6b9844579d 100755 --- a/installation/docker-commands/create-gateway.sh +++ b/installation/docker-commands/create-gateway.sh @@ -148,6 +148,7 @@ prompt_token_list_source () { 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 @@ -206,13 +207,14 @@ prompt_eth_gasstation_setup () { prompt_eth_gasstation_setup fi fi + echo } prompt_eth_gasstation_setup prompt_balancer_setup () { - # Ask the user for the max balancer pool to use - echo - read -p " Enter the maximum balancer swap pool (default = \"4\") >>> " BALANCER_MAX_SWAPS + # 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" @@ -220,10 +222,21 @@ prompt_balancer_setup () { fi } +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 prompt_balancer_setup + prompt_uniswap_setup fi # Ask the user for ethereum network @@ -325,6 +338,7 @@ 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" @@ -365,7 +379,7 @@ echo "BALANCER_MAX_SWAPS=$BALANCER_MAX_SWAPS" >> $ENV_FILE echo "" >> $ENV_FILE echo "# Uniswap Settings" >> $ENV_FILE echo "UNISWAP_ROUTER=$UNISWAP_ROUTER" >> $ENV_FILE -echo "UNISWAP_ALLOWED_SLIPPAGE=1" >> $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 From b495ea002e2805f52ab92e6f8f19e1a80e028829 Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Mon, 29 Mar 2021 23:10:30 +0800 Subject: [PATCH 30/34] (update) update AascendEX logo --- assets/ascend_ex_logo.png | Bin 8570 -> 3950 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/ascend_ex_logo.png b/assets/ascend_ex_logo.png index 23758c1611a603867832b71f5bd84e7565d7c41d..c30f757cf4dd96deff3c793ce1c2278f921afa2e 100644 GIT binary patch literal 3950 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 mbmj}#^k}AH%@2R394%I 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 From 2ce566056f01ab19f14633d444ce14b732c825bf Mon Sep 17 00:00:00 2001 From: SGoh <51857968+sdgoh@users.noreply.github.com> Date: Wed, 31 Mar 2021 14:08:38 +0800 Subject: [PATCH 31/34] Update hummingbot/strategy/amm_arb/amm_arb.py Co-authored-by: vic-en <31972210+vic-en@users.noreply.github.com> --- hummingbot/strategy/amm_arb/amm_arb.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hummingbot/strategy/amm_arb/amm_arb.py b/hummingbot/strategy/amm_arb/amm_arb.py index b544fe41d4..381cd991cc 100644 --- a/hummingbot/strategy/amm_arb/amm_arb.py +++ b/hummingbot/strategy/amm_arb/amm_arb.py @@ -255,9 +255,9 @@ async def format_status(self) -> str: sell_price = await market.get_quote_price(trading_pair, False, self._order_amount) # check for unavailable price data - buy_price = float(buy_price) if buy_price is not None else '-' - sell_price = float(sell_price) if sell_price is not None else '-' - mid_price = float((buy_price + sell_price) / 2) if '-' not in [buy_price, sell_price] else '-' + 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, From 6ee898e5580ebaab0c9b334746fa634d89f08aa1 Mon Sep 17 00:00:00 2001 From: SGoh <51857968+sdgoh@users.noreply.github.com> Date: Wed, 31 Mar 2021 14:09:06 +0800 Subject: [PATCH 32/34] Update hummingbot/strategy/amm_arb/data_types.py Co-authored-by: vic-en <31972210+vic-en@users.noreply.github.com> --- hummingbot/strategy/amm_arb/data_types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hummingbot/strategy/amm_arb/data_types.py b/hummingbot/strategy/amm_arb/data_types.py index 93b5503c02..70375d4b93 100644 --- a/hummingbot/strategy/amm_arb/data_types.py +++ b/hummingbot/strategy/amm_arb/data_types.py @@ -79,9 +79,7 @@ def profit_pct(self, account_for_fee: bool = False, first_side_quote_eth_rate: D sell_gained_net = (sell.amount * sell.quote_price) - sell_fee_amount buy_spent_net = (buy.amount * buy.quote_price) + buy_fee_amount - profit_percentage = ((sell_gained_net - buy_spent_net) / buy_spent_net) if buy_spent_net != s_decimal_0 else s_decimal_0 - - return profit_percentage + return ((sell_gained_net - buy_spent_net) / buy_spent_net) if buy_spent_net != s_decimal_0 else s_decimal_0 def __repr__(self): return f"First Side - {self.first_side}\nSecond Side - {self.second_side}" From 7e694d9ff205174e916b0452602feb33b83f5fa1 Mon Sep 17 00:00:00 2001 From: sdgoh Date: Wed, 31 Mar 2021 14:35:15 +0800 Subject: [PATCH 33/34] resolve conflict of missing token address file path --- hummingbot/client/config/config_helpers.py | 2 ++ hummingbot/client/settings.py | 1 + 2 files changed, 3 insertions(+) diff --git a/hummingbot/client/config/config_helpers.py b/hummingbot/client/config/config_helpers.py index 834a2b872a..2f8b491c75 100644 --- a/hummingbot/client/config/config_helpers.py +++ b/hummingbot/client/config/config_helpers.py @@ -11,6 +11,7 @@ ) from collections import OrderedDict import json +import requests from typing import ( Any, Callable, @@ -31,6 +32,7 @@ CONF_FILE_PATH, CONF_POSTFIX, CONF_PREFIX, + TOKEN_ADDRESSES_FILE_PATH, CONNECTOR_SETTINGS ) from hummingbot.client.config.security import Security diff --git a/hummingbot/client/settings.py b/hummingbot/client/settings.py index 3452d5fa57..c832bcc7e4 100644 --- a/hummingbot/client/settings.py +++ b/hummingbot/client/settings.py @@ -23,6 +23,7 @@ ENCYPTED_CONF_POSTFIX = ".json" GLOBAL_CONFIG_PATH = "conf/conf_global.yml" TRADE_FEES_CONFIG_PATH = "conf/conf_fee_overrides.yml" +TOKEN_ADDRESSES_FILE_PATH = "conf/erc20_tokens_override.json" DEFAULT_KEY_FILE_PATH = "conf/" DEFAULT_LOG_FILE_PATH = "logs/" DEFAULT_ETHEREUM_RPC_URL = "https://mainnet.coinalpha.com/hummingbot-test-node" From 7524959fba31add3e538c6c2680f64f3d40b124e Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 31 Mar 2021 11:22:14 +0100 Subject: [PATCH 34/34] (feat) make xdai provider configurable --- installation/docker-commands/create-gateway.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/installation/docker-commands/create-gateway.sh b/installation/docker-commands/create-gateway.sh index 6b9844579d..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 @@ -95,7 +95,7 @@ prompt_ethereum_setup () { # chain selection if [ "$ETHEREUM_CHAIN" == "" ] then - ETHEREUM_CHAIN="mainnet" + ETHEREUM_CHAIN="mainnet" fi if [[ "$ETHEREUM_CHAIN" != "mainnet" && "$ETHEREUM_CHAIN" != "kovan" ]] then @@ -239,6 +239,18 @@ then 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 @@ -392,7 +404,7 @@ echo "TERRA_CHAIN=$TERRA_CHAIN" >> $ENV_FILE # perpeptual finance config echo "" >> $ENV_FILE echo "# Perpeptual Settings" >> $ENV_FILE -echo "XDAI_PROVIDER=https://rpc.xdaichain.com" >> $ENV_FILE +echo "XDAI_PROVIDER=$XDAI_PROVIDER" >> $ENV_FILE echo "" >> $ENV_FILE