diff --git a/README.md b/README.md index 44a07a5023..ab25df63e3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Help us **democratize high-frequency trading** and make powerful trading algorit ## Quick Links * [Website and Docs](https://hummingbot.org): Official Hummingbot website and documentation -* [Installation](https://hummingbot.org/installation/): Install Hummingbot on various platforms +* [Installation](https://hummingbot.org/installation/docker/): Install Hummingbot on various platforms * [FAQs](https://hummingbot.org/faq/): Answers to all your burning questions * [Botcamp](https://hummingbot.org/botcamp/): Learn how build your own custom HFT strategy in Hummingbot with our hands-on bootcamp! * [Newsletter](https://hummingbot.substack.com): Get our monthly newletter whenever we ship a new release @@ -38,7 +38,7 @@ Help us **democratize high-frequency trading** and make powerful trading algorit We recommend installing Hummingbot using Docker if you want the simplest, easiest installation method and don't need to modify the Hummingbot codebase. -**Prerequesites:** +**Prerequisites:** * MacOS 10.12.6+ / Linux (Ubuntu 20.04+, Debian 10+) / Windows 10+ * Memory: 4 GB RAM per instance @@ -56,7 +56,7 @@ docker attach hummingbot We recommend installing Hummingbot from source if you want to customize or extend the Hummingbot codebase, build new components like connectors or strategies, and/or learn how Hummingbot works at a deeper, technical level. -**Prerequesites:** +**Prerequisites:** * MacOS 10.12.6+ / Linux (Ubuntu 20.04+, Debian 10+) * Memory: 4 GB RAM per instance @@ -72,7 +72,7 @@ conda activate hummingbot ./start ``` -See [Installation](https://hummingbot.org/installation/) for detailed guides for each OS. +See [Installation](https://hummingbot.org/installation/linux/) for detailed guides for each OS. ## Architecture diff --git a/controllers/generic/__init__.py b/controllers/generic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/controllers/generic/xemm_multiple_levels.py b/controllers/generic/xemm_multiple_levels.py new file mode 100644 index 0000000000..d13c050b16 --- /dev/null +++ b/controllers/generic/xemm_multiple_levels.py @@ -0,0 +1,146 @@ +import time +from decimal import Decimal +from typing import Dict, List, Set + +import pandas as pd +from pydantic import Field, validator + +from hummingbot.client.config.config_data_types import ClientFieldData +from hummingbot.client.ui.interface_utils import format_df_for_printout +from hummingbot.core.data_type.common import PriceType, TradeType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.controller_base import ControllerBase, ControllerConfigBase +from hummingbot.smart_components.executors.data_types import ConnectorPair +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.smart_components.models.executor_actions import CreateExecutorAction, ExecutorAction + + +class XEMMMultipleLevelsConfig(ControllerConfigBase): + controller_name: str = "xemm_multiple_levels" + candles_config: List[CandlesConfig] = [] + maker_connector: str = Field( + default="kucoin", + client_data=ClientFieldData( + prompt=lambda e: "Enter the maker connector: ", + prompt_on_new=True + )) + maker_trading_pair: str = Field( + default="LBR-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the maker trading pair: ", + prompt_on_new=True + )) + taker_connector: str = Field( + default="okx", + client_data=ClientFieldData( + prompt=lambda e: "Enter the taker connector: ", + prompt_on_new=True + )) + taker_trading_pair: str = Field( + default="LBR-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the taker trading pair: ", + prompt_on_new=True + )) + buy_levels_targets_amount: List[List[Decimal]] = Field( + default="0.003,10-0.006,20-0.009,30", + client_data=ClientFieldData( + prompt=lambda e: "Enter the buy levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", + prompt_on_new=True + )) + sell_levels_targets_amount: List[List[Decimal]] = Field( + default="0.003,10-0.006,20-0.009,30", + client_data=ClientFieldData( + prompt=lambda e: "Enter the sell levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", + prompt_on_new=True + )) + min_profitability: Decimal = Field( + default=0.002, + client_data=ClientFieldData( + prompt=lambda e: "Enter the minimum profitability: ", + prompt_on_new=True + )) + max_profitability: Decimal = Field( + default=0.01, + client_data=ClientFieldData( + prompt=lambda e: "Enter the maximum profitability: ", + prompt_on_new=True + )) + + @validator("buy_levels_targets_amount", "sell_levels_targets_amount", pre=True, always=True) + def validate_levels_targets_amount(cls, v, values): + if isinstance(v, str): + v = [list(map(Decimal, x.split(","))) for x in v.split("-")] + return v + + def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]: + if self.maker_connector not in markets: + markets[self.maker_connector] = set() + markets[self.maker_connector].add(self.maker_trading_pair) + if self.taker_connector not in markets: + markets[self.taker_connector] = set() + markets[self.taker_connector].add(self.taker_trading_pair) + return markets + + +class XEMMMultipleLevels(ControllerBase): + + def __init__(self, config: XEMMMultipleLevelsConfig, *args, **kwargs): + self.config = config + self.buy_levels_targets_amount = config.buy_levels_targets_amount + self.sell_levels_targets_amount = config.sell_levels_targets_amount + super().__init__(config, *args, **kwargs) + + async def update_processed_data(self): + pass + + def determine_executor_actions(self) -> List[ExecutorAction]: + executor_actions = [] + mid_price = self.market_data_provider.get_price_by_type(self.config.maker_connector, self.config.maker_trading_pair, PriceType.MidPrice) + active_buy_executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.BUY + ) + active_sell_executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.SELL + ) + for target_profitability, amount in self.buy_levels_targets_amount: + active_buy_executors_target = [e.config.target_profitability == target_profitability for e in active_buy_executors] + if len(active_buy_executors_target) == 0: + config = XEMMExecutorConfig( + controller_id=self.config.id, + timestamp=time.time(), + buying_market=ConnectorPair(connector_name=self.config.maker_connector, + trading_pair=self.config.maker_trading_pair), + selling_market=ConnectorPair(connector_name=self.config.taker_connector, + trading_pair=self.config.taker_trading_pair), + maker_side=TradeType.BUY, + order_amount=amount / mid_price, + min_profitability=self.config.min_profitability, + target_profitability=target_profitability, + max_profitability=self.config.max_profitability + ) + executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id)) + for target_profitability, amount in self.sell_levels_targets_amount: + active_sell_executors_target = [e.config.target_profitability == target_profitability for e in active_sell_executors] + if len(active_sell_executors_target) == 0: + config = XEMMExecutorConfig( + controller_id=self.config.id, + timestamp=time.time(), + buying_market=ConnectorPair(connector_name=self.config.taker_connector, + trading_pair=self.config.taker_trading_pair), + selling_market=ConnectorPair(connector_name=self.config.maker_connector, + trading_pair=self.config.maker_trading_pair), + maker_side=TradeType.SELL, + order_amount=amount / mid_price, + min_profitability=self.config.min_profitability, + target_profitability=target_profitability, + max_profitability=self.config.max_profitability + ) + executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id)) + return executor_actions + + def to_format_status(self) -> List[str]: + all_executors_custom_info = pd.DataFrame(e.custom_info for e in self.executors_info) + return [format_df_for_printout(all_executors_custom_info, table_format="psql", )] diff --git a/controllers/market_making/dman_maker_v2.py b/controllers/market_making/dman_maker_v2.py new file mode 100644 index 0000000000..12365ec6ca --- /dev/null +++ b/controllers/market_making/dman_maker_v2.py @@ -0,0 +1,138 @@ +import time +from decimal import Decimal +from typing import List, Optional + +import pandas_ta as ta # noqa: F401 +from pydantic import Field, validator + +from hummingbot.client.config.config_data_types import ClientFieldData +from hummingbot.core.data_type.common import TradeType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.market_making_controller_base import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, +) +from hummingbot.smart_components.executors.dca_executor.data_types import DCAExecutorConfig, DCAMode +from hummingbot.smart_components.models.executor_actions import ExecutorAction, StopExecutorAction + + +class DManMakerV2Config(MarketMakingControllerConfigBase): + """ + Configuration required to run the D-Man Maker V2 strategy. + """ + controller_name: str = "dman_maker_v2" + candles_config: List[CandlesConfig] = [] + + # DCA configuration + dca_spreads: List[Decimal] = Field( + default="0.01,0.02,0.04,0.08", + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter a comma-separated list of spreads for each DCA level: ")) + dca_amounts: List[Decimal] = Field( + default="0.1,0.2,0.4,0.8", + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter a comma-separated list of amounts for each DCA level: ")) + time_limit: int = Field( + default=60 * 60 * 24 * 7, gt=0, + client_data=ClientFieldData( + prompt=lambda mi: "Enter the time limit for each DCA level: ", + prompt_on_new=False)) + stop_loss: Decimal = Field( + default=Decimal("0.03"), gt=0, + client_data=ClientFieldData( + prompt=lambda mi: "Enter the stop loss (as a decimal, e.g., 0.03 for 3%): ", + prompt_on_new=True)) + top_executor_refresh_time: Optional[float] = Field( + default=None, + client_data=ClientFieldData( + is_updatable=True, + prompt_on_new=False)) + executor_activation_bounds: Optional[List[Decimal]] = Field( + default=None, + client_data=ClientFieldData( + is_updatable=True, + prompt=lambda mi: "Enter the activation bounds for the orders " + "(e.g., 0.01 activates the next order when the price is closer than 1%): ", + prompt_on_new=False)) + + @validator("executor_activation_bounds", pre=True, always=True) + def parse_activation_bounds(cls, v): + if isinstance(v, list): + return [Decimal(val) for val in v] + elif isinstance(v, str): + if v == "": + return None + return [Decimal(val) for val in v.split(",")] + return v + + @validator('dca_spreads', pre=True, always=True) + def parse_spreads(cls, v): + if v is None: + return [] + if isinstance(v, str): + if v == "": + return [] + return [float(x.strip()) for x in v.split(',')] + return v + + @validator('dca_amounts', pre=True, always=True) + def parse_and_validate_amounts(cls, v, values, field): + if v is None or v == "": + return [1 for _ in values[values['dca_spreads']]] + if isinstance(v, str): + return [float(x.strip()) for x in v.split(',')] + elif isinstance(v, list) and len(v) != len(values['dca_spreads']): + raise ValueError( + f"The number of {field.name} must match the number of {values['dca_spreads']}.") + return v + + +class DManMakerV2(MarketMakingControllerBase): + def __init__(self, config: DManMakerV2Config, *args, **kwargs): + super().__init__(config, *args, **kwargs) + self.config = config + self.dca_amounts_pct = [Decimal(amount) / sum(self.config.dca_amounts) for amount in self.config.dca_amounts] + self.spreads = self.config.dca_spreads + + def first_level_refresh_condition(self, executor): + if self.config.top_executor_refresh_time is not None: + if self.get_level_from_level_id(executor.custom_info["level_id"]) == 0: + return time.time() - executor.timestamp > self.config.top_executor_refresh_time + return False + + def order_level_refresh_condition(self, executor): + return time.time() - executor.timestamp > self.config.executor_refresh_time + + def executors_to_refresh(self) -> List[ExecutorAction]: + executors_to_refresh = self.filter_executors( + executors=self.executors_info, + filter_func=lambda x: not x.is_trading and x.is_active and (self.order_level_refresh_condition(x) or self.first_level_refresh_condition(x))) + return [StopExecutorAction( + controller_id=self.config.id, + executor_id=executor.id) for executor in executors_to_refresh] + + def get_executor_config(self, level_id: str, price: Decimal, amount: Decimal): + trade_type = self.get_trade_type_from_level_id(level_id) + if trade_type == TradeType.BUY: + prices = [price * (1 - spread) for spread in self.spreads] + else: + prices = [price * (1 + spread) for spread in self.spreads] + amounts = [amount * pct for pct in self.dca_amounts_pct] + amounts_quote = [amount * price for amount, price in zip(amounts, prices)] + return DCAExecutorConfig( + timestamp=time.time(), + connector_name=self.config.connector_name, + trading_pair=self.config.trading_pair, + mode=DCAMode.MAKER, + side=trade_type, + prices=prices, + amounts_quote=amounts_quote, + level_id=level_id, + time_limit=self.config.time_limit, + stop_loss=self.config.stop_loss, + trailing_stop=self.config.trailing_stop, + activation_bounds=self.config.executor_activation_bounds, + leverage=self.config.leverage, + ) diff --git a/controllers/market_making/pmm_simple.py b/controllers/market_making/pmm_simple.py index d3e7683de8..2524180420 100644 --- a/controllers/market_making/pmm_simple.py +++ b/controllers/market_making/pmm_simple.py @@ -18,7 +18,7 @@ class PMMSimpleConfig(MarketMakingControllerConfigBase): controller_name = "pmm_simple" # As this controller is a simple version of the PMM, we are not using the candles feed candles_config: List[CandlesConfig] = Field(default=[], client_data=ClientFieldData(prompt_on_new=False)) - top_order_refresh_time: Optional[float] = Field( + top_executor_refresh_time: Optional[float] = Field( default=None, client_data=ClientFieldData( is_updatable=True, @@ -31,9 +31,9 @@ def __init__(self, config: PMMSimpleConfig, *args, **kwargs): self.config = config def first_level_refresh_condition(self, executor): - if self.config.top_order_refresh_time is not None: - if self.get_level_from_level_id(executor.custom_info["level_id"]) == 1: - return time.time() - executor.timestamp > self.config.top_order_refresh_time + if self.config.top_executor_refresh_time is not None: + if self.get_level_from_level_id(executor.custom_info["level_id"]) == 0: + return time.time() - executor.timestamp > self.config.top_executor_refresh_time return False def order_level_refresh_condition(self, executor): diff --git a/docker-compose.yml b/docker-compose.yml index f711045771..4d551e652b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,10 @@ version: "3.9" services: hummingbot: container_name: hummingbot - build: - context: . - dockerfile: Dockerfile + image: hummingbot/hummingbot:latest +# build: Uncomment this and comment image if you want to build it locally +# context: . +# dockerfile: Dockerfile volumes: - ./conf:/home/hummingbot/conf - ./conf/connectors:/home/hummingbot/conf/connectors diff --git a/hummingbot/client/config/client_config_map.py b/hummingbot/client/config/client_config_map.py index 4d7fc1c677..d3f022efee 100644 --- a/hummingbot/client/config/client_config_map.py +++ b/hummingbot/client/config/client_config_map.py @@ -768,6 +768,17 @@ class Config: title = "binance" +class BinanceUSRateSourceMode(ExchangeRateSourceModeBase): + name: str = Field( + default="binance_us", + const=True, + client_data=None, + ) + + class Config: + title = "binance_us" + + class CubeRateSourceMode(ExchangeRateSourceModeBase): name: str = Field( default="cube", @@ -921,13 +932,26 @@ class Config: title: str = "gate_io" +class CoinbaseAdvancedTradeRateSourceMode(ExchangeRateSourceModeBase): + name: str = Field( + default="coinbase_advanced_trade", + const=True, + client_data=None, + ) + + class Config: + title: str = "coinbase_advanced_trade" + + RATE_SOURCE_MODES = { AscendExRateSourceMode.Config.title: AscendExRateSourceMode, BinanceRateSourceMode.Config.title: BinanceRateSourceMode, + BinanceUSRateSourceMode.Config.title: BinanceUSRateSourceMode, CoinGeckoRateSourceMode.Config.title: CoinGeckoRateSourceMode, CoinCapRateSourceMode.Config.title: CoinCapRateSourceMode, KuCoinRateSourceMode.Config.title: KuCoinRateSourceMode, GateIoRateSourceMode.Config.title: GateIoRateSourceMode, + CoinbaseAdvancedTradeRateSourceMode.Config.title: CoinbaseAdvancedTradeRateSourceMode, CubeRateSourceMode.Config.title: CubeRateSourceMode, } diff --git a/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_derivative.py b/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_derivative.py index 1af69aa3de..f31642d45d 100644 --- a/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_derivative.py +++ b/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_derivative.py @@ -179,6 +179,7 @@ def _get_fee(self, quote_currency: str, order_type: OrderType, order_side: TradeType, + position_action: PositionAction, amount: Decimal, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> TradeFeeBase: diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_api_order_book_data_source.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_api_order_book_data_source.py index f28ff61f08..09c34f422b 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_api_order_book_data_source.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_api_order_book_data_source.py @@ -7,7 +7,7 @@ import hummingbot.connector.derivative.hyperliquid_perpetual.hyperliquid_perpetual_constants as CONSTANTS import hummingbot.connector.derivative.hyperliquid_perpetual.hyperliquid_perpetual_web_utils as web_utils from hummingbot.core.data_type.common import TradeType -from hummingbot.core.data_type.funding_info import FundingInfo +from hummingbot.core.data_type.funding_info import FundingInfo, FundingInfoUpdate from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType from hummingbot.core.data_type.perpetual_api_order_book_data_source import PerpetualAPIOrderBookDataSource from hummingbot.core.web_assistant.connections.data_types import WSJSONRequest @@ -61,6 +61,29 @@ async def get_funding_info(self, trading_pair: str) -> FundingInfo: ) return funding_info + async def listen_for_funding_info(self, output: asyncio.Queue): + """ + Reads the funding info events queue and updates the local funding info information. + """ + while True: + try: + for trading_pair in self._trading_pairs: + funding_info = await self.get_funding_info(trading_pair) + funding_info_update = FundingInfoUpdate( + trading_pair=trading_pair, + index_price=funding_info.index_price, + mark_price=funding_info.mark_price, + next_funding_utc_timestamp=funding_info.next_funding_utc_timestamp, + rate=funding_info.rate, + ) + output.put_nowait(funding_info_update) + except asyncio.CancelledError: + raise + except Exception: + self.logger().exception("Unexpected error when processing public funding info updates from exchange") + finally: + await asyncio.sleep(CONSTANTS.FUNDING_RATE_UPDATE_INTERNAL_SECOND) + async def _request_order_book_snapshot(self, trading_pair: str) -> Dict[str, Any]: ex_trading_pair = await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair) coin = ex_trading_pair.split("-")[0] diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_constants.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_constants.py index d172c98233..c72fbb8dcc 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_constants.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_constants.py @@ -18,7 +18,7 @@ TESTNET_WS_URL = "wss://api.hyperliquid-testnet.xyz/ws" -FUNDING_RATE_INTERNAL_MIL_SECOND = 3600 +FUNDING_RATE_UPDATE_INTERNAL_SECOND = 60 CURRENCY = "USD" diff --git a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py index ca07fbea4e..d6aa24ce35 100644 --- a/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py +++ b/hummingbot/connector/derivative/hyperliquid_perpetual/hyperliquid_perpetual_derivative.py @@ -231,6 +231,7 @@ def _get_fee(self, quote_currency: str, order_type: OrderType, order_side: TradeType, + position_action: PositionAction, amount: Decimal, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> TradeFeeBase: diff --git a/hummingbot/connector/exchange/binance/binance_utils.py b/hummingbot/connector/exchange/binance/binance_utils.py index b0e47c4da0..6b3676066d 100644 --- a/hummingbot/connector/exchange/binance/binance_utils.py +++ b/hummingbot/connector/exchange/binance/binance_utils.py @@ -22,7 +22,20 @@ def is_exchange_information_valid(exchange_info: Dict[str, Any]) -> bool: :param exchange_info: the exchange information for a trading pair :return: True if the trading pair is enabled, False otherwise """ - return exchange_info.get("status", None) == "TRADING" and "SPOT" in exchange_info.get("permissions", list()) + is_spot = False + is_trading = False + + if exchange_info.get("status", None) == "TRADING": + is_trading = True + + permissions_sets = exchange_info.get("permissionSets", list()) + for permission_set in permissions_sets: + # PermissionSet is a list, find if in this list we have "SPOT" value or not + if "SPOT" in permission_set: + is_spot = True + break + + return is_trading and is_spot class BinanceConfigMap(BaseConnectorConfigMap): diff --git a/hummingbot/connector/exchange/kucoin/kucoin_constants.py b/hummingbot/connector/exchange/kucoin/kucoin_constants.py index af2a62df6f..da3e3370ed 100644 --- a/hummingbot/connector/exchange/kucoin/kucoin_constants.py +++ b/hummingbot/connector/exchange/kucoin/kucoin_constants.py @@ -12,6 +12,7 @@ # REST endpoints BASE_PATH_URL = { "main": "https://api.kucoin.com", + "hft": "https://api.kucoin.com", } PUBLIC_WS_DATA_PATH_URL = "/api/v1/bullet-public" PRIVATE_WS_DATA_PATH_URL = "/api/v1/bullet-private" @@ -21,9 +22,11 @@ SERVER_TIME_PATH_URL = "/api/v1/timestamp" SYMBOLS_PATH_URL = "/api/v2/symbols" ORDERS_PATH_URL = "/api/v1/orders" +ORDERS_PATH_URL_HFT = "/api/v1/hf/orders" FEE_PATH_URL = "/api/v1/trade-fees" ALL_TICKERS_PATH_URL = "/api/v1/market/allTickers" FILLS_PATH_URL = "/api/v1/fills" +FILLS_PATH_URL_HFT = "/api/v1/hf/fills" LIMIT_FILLS_PATH_URL = "/api/v1/limit/fills" ORDER_CLIENT_ORDER_PATH_URL = "/api/v1/order/client-order" @@ -61,5 +64,7 @@ RateLimit(limit_id=POST_ORDER_LIMIT_ID, limit=45, time_interval=3), RateLimit(limit_id=DELETE_ORDER_LIMIT_ID, limit=60, time_interval=3), RateLimit(limit_id=ORDERS_PATH_URL, limit=45, time_interval=3), + RateLimit(limit_id=ORDERS_PATH_URL_HFT, limit=45, time_interval=3), RateLimit(limit_id=FILLS_PATH_URL, limit=9, time_interval=3), + RateLimit(limit_id=FILLS_PATH_URL_HFT, limit=9, time_interval=3), ] diff --git a/hummingbot/connector/exchange/kucoin/kucoin_exchange.py b/hummingbot/connector/exchange/kucoin/kucoin_exchange.py index bb0395f6d2..400c45f14c 100644 --- a/hummingbot/connector/exchange/kucoin/kucoin_exchange.py +++ b/hummingbot/connector/exchange/kucoin/kucoin_exchange.py @@ -89,6 +89,14 @@ def trading_pairs_request_path(self): def check_network_request_path(self): return CONSTANTS.SERVER_TIME_PATH_URL + @property + def orders_path_url(self): + return CONSTANTS.ORDERS_PATH_URL_HFT if self._domain == "hft" else CONSTANTS.ORDERS_PATH_URL + + @property + def fills_path_url(self): + return CONSTANTS.FILLS_PATH_URL_HFT if self.domain == "hft" else CONSTANTS.FILLS_PATH_URL + @property def trading_pairs(self): return self._trading_pairs @@ -193,7 +201,6 @@ async def _place_order(self, order_type: OrderType, price: Decimal, **kwargs) -> Tuple[str, float]: - path_url = CONSTANTS.ORDERS_PATH_URL side = trade_type.name.lower() order_type_str = "market" if order_type == OrderType.MARKET else "limit" data = { @@ -209,7 +216,7 @@ async def _place_order(self, data["price"] = str(price) data["postOnly"] = True exchange_order_id = await self._api_post( - path_url=path_url, + path_url=self.orders_path_url, data=data, is_auth_required=True, limit_id=CONSTANTS.POST_ORDER_LIMIT_ID, @@ -223,12 +230,15 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): This implementation specific function is called by _cancel, and returns True if successful """ exchange_order_id = await tracked_order.get_exchange_order_id() + params = {"symbol": tracked_order.trading_pair} if self.domain == "hft" else None cancel_result = await self._api_delete( - f"{CONSTANTS.ORDERS_PATH_URL}/{exchange_order_id}", + f"{self.orders_path_url}/{exchange_order_id}", + params=params, is_auth_required=True, limit_id=CONSTANTS.DELETE_ORDER_LIMIT_ID ) - if tracked_order.exchange_order_id in cancel_result["data"].get("cancelledOrderIds", []): + response_param = "orderId" if self.domain == "hft" else "cancelledOrderIds" + if tracked_order.exchange_order_id in cancel_result["data"].get(response_param, []): return True return False @@ -316,10 +326,11 @@ async def _user_stream_event_listener(self): async def _update_balances(self): local_asset_names = set(self._account_balances.keys()) remote_asset_names = set() + account_type = "trade_hf" if self.domain == "hft" else "trade" response = await self._api_get( path_url=CONSTANTS.ACCOUNTS_PATH_URL, - params={"type": "trade"}, + params={"type": account_type}, is_auth_required=True) if response: @@ -406,7 +417,7 @@ async def _all_trades_updates(self, orders: List[InFlightOrder]) -> List[TradeUp # "If you only specified the start time, the system will automatically # calculate the end time (end time = start time + 7 * 24 hours)" all_fills_response = await self._api_get( - path_url=CONSTANTS.FILLS_PATH_URL, + path_url=self.fills_path_url, params={ "pageSize": 500, "startAt": self._last_order_fill_ts_s * 1000, @@ -448,7 +459,7 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade if order.exchange_order_id is not None: exchange_order_id = order.exchange_order_id all_fills_response = await self._api_get( - path_url=CONSTANTS.FILLS_PATH_URL, + path_url=self.fills_path_url, params={ "orderId": exchange_order_id, "pageSize": 500, @@ -479,13 +490,15 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: exchange_order_id = await tracked_order.get_exchange_order_id() + params = {"symbol": tracked_order.trading_pair} if self.domain == "hft" else None updated_order_data = await self._api_get( - path_url=f"{CONSTANTS.ORDERS_PATH_URL}/{exchange_order_id}", + path_url=f"{self.orders_path_url}/{exchange_order_id}", is_auth_required=True, + params=params, limit_id=CONSTANTS.GET_ORDER_LIMIT_ID) ordered_canceled = updated_order_data["data"]["cancelExist"] - is_active = updated_order_data["data"]["isActive"] + is_active = updated_order_data["data"]["active"] if self.domain == "hft" else updated_order_data["data"]["isActive"] op_type = updated_order_data["data"]["opType"] new_state = tracked_order.current_state diff --git a/hummingbot/connector/exchange/kucoin/kucoin_utils.py b/hummingbot/connector/exchange/kucoin/kucoin_utils.py index ba96c43ef5..069da94e81 100644 --- a/hummingbot/connector/exchange/kucoin/kucoin_utils.py +++ b/hummingbot/connector/exchange/kucoin/kucoin_utils.py @@ -62,3 +62,45 @@ class Config: KEYS = KuCoinConfigMap.construct() + +OTHER_DOMAINS = ["kucoin_hft"] +OTHER_DOMAINS_PARAMETER = {"kucoin_hft": "hft"} +OTHER_DOMAINS_EXAMPLE_PAIR = {"kucoin_hft": "ETH-USDT"} +OTHER_DOMAINS_DEFAULT_FEES = {"kucoin_hft": DEFAULT_FEES} + + +class KuCoinHFTConfigMap(BaseConnectorConfigMap): + connector: str = Field(default="kucoin_hft", client_data=None) + kucoin_hft_api_key: SecretStr = Field( + default=..., + client_data=ClientFieldData( + prompt=lambda cm: "Enter your KuCoin HFT API key", + is_secure=True, + is_connect_key=True, + prompt_on_new=True, + ) + ) + kucoin_hft_secret_key: SecretStr = Field( + default=..., + client_data=ClientFieldData( + prompt=lambda cm: "Enter your KuCoin HFT secret key", + is_secure=True, + is_connect_key=True, + prompt_on_new=True, + ) + ) + kucoin_hft_passphrase: SecretStr = Field( + default=..., + client_data=ClientFieldData( + prompt=lambda cm: "Enter your KuCoin HFT passphrase", + is_secure=True, + is_connect_key=True, + prompt_on_new=True, + ) + ) + + class Config: + title = "kucoin_hft" + + +OTHER_DOMAINS_KEYS = {"kucoin_hft": KuCoinHFTConfigMap.construct()} diff --git a/hummingbot/connector/exchange/okx/okx_constants.py b/hummingbot/connector/exchange/okx/okx_constants.py index d9307f4620..271b683195 100644 --- a/hummingbot/connector/exchange/okx/okx_constants.py +++ b/hummingbot/connector/exchange/okx/okx_constants.py @@ -1,6 +1,7 @@ import sys from hummingbot.core.api_throttler.data_types import RateLimit +from hummingbot.core.data_type.common import OrderType from hummingbot.core.data_type.in_flight_order import OrderState CLIENT_ID_PREFIX = "93027a12dac34fBC" @@ -54,6 +55,11 @@ "canceled": OrderState.CANCELED, } +ORDER_TYPE_MAP = { + OrderType.LIMIT: "limit", + OrderType.MARKET: "market", +} + NO_LIMIT = sys.maxsize RATE_LIMITS = [ diff --git a/hummingbot/connector/exchange/okx/okx_exchange.py b/hummingbot/connector/exchange/okx/okx_exchange.py index afeb954f64..effccf6eb4 100644 --- a/hummingbot/connector/exchange/okx/okx_exchange.py +++ b/hummingbot/connector/exchange/okx/okx_exchange.py @@ -97,7 +97,7 @@ def is_trading_required(self) -> bool: return self._trading_required def supported_order_types(self): - return [OrderType.LIMIT, OrderType.LIMIT_MAKER] + return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception): error_description = str(request_exception) @@ -184,15 +184,17 @@ async def _place_order(self, order_type: OrderType, price: Decimal, **kwargs) -> Tuple[str, float]: + data = { "clOrdId": order_id, "tdMode": "cash", - "ordType": "limit", + "ordType": CONSTANTS.ORDER_TYPE_MAP[order_type], "side": trade_type.name.lower(), "instId": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair), "sz": str(amount), - "px": str(price) } + if order_type.is_limit_type(): + data["px"] = str(price) exchange_order_id = await self._api_request( path_url=CONSTANTS.OKX_PLACE_ORDER_PATH, diff --git a/hummingbot/connector/exchange/vertex/vertex_api_order_book_data_source.py b/hummingbot/connector/exchange/vertex/vertex_api_order_book_data_source.py index 214abaddda..129b755c0a 100644 --- a/hummingbot/connector/exchange/vertex/vertex_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/vertex/vertex_api_order_book_data_source.py @@ -163,7 +163,7 @@ async def _process_websocket_messages(self, websocket_assistant: WSAssistant): self._last_ws_message_sent_timestamp = ping_time async def _connected_websocket_assistant(self) -> WSAssistant: - ws_url = f"{CONSTANTS.WSS_URLS[self._domain]}{CONSTANTS.WS_SUBSCRIBE_PATH_URL}" + ws_url = f"{CONSTANTS.WS_SUBSCRIBE_URLS[self._domain]}" self._ping_interval = CONSTANTS.HEARTBEAT_TIME_INTERVAL diff --git a/hummingbot/connector/exchange/vertex/vertex_api_user_stream_data_source.py b/hummingbot/connector/exchange/vertex/vertex_api_user_stream_data_source.py index 9684fa6532..5def25d9bb 100644 --- a/hummingbot/connector/exchange/vertex/vertex_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/vertex/vertex_api_user_stream_data_source.py @@ -39,7 +39,7 @@ def __init__( self._last_ws_message_sent_timestamp = 0 async def _connected_websocket_assistant(self) -> WSAssistant: - ws_url = f"{CONSTANTS.WSS_URLS[self._domain]}{CONSTANTS.WS_SUBSCRIBE_PATH_URL}" + ws_url = f"{CONSTANTS.WS_SUBSCRIBE_URLS[self._domain]}" self._ping_interval = CONSTANTS.HEARTBEAT_TIME_INTERVAL ws: WSAssistant = await self._api_factory.get_ws_assistant() diff --git a/hummingbot/connector/exchange/vertex/vertex_constants.py b/hummingbot/connector/exchange/vertex/vertex_constants.py index 56ca536b13..1646ff8373 100644 --- a/hummingbot/connector/exchange/vertex/vertex_constants.py +++ b/hummingbot/connector/exchange/vertex/vertex_constants.py @@ -21,13 +21,23 @@ QUOTE = "USDC" BASE_URLS = { - DEFAULT_DOMAIN: "https://prod.vertexprotocol-backend.com", - TESTNET_DOMAIN: "https://test.vertexprotocol-backend.com", + DEFAULT_DOMAIN: "https://gateway.prod.vertexprotocol.com/v1", + TESTNET_DOMAIN: "https://gateway.sepolia-test.vertexprotocol.com/v1", } WSS_URLS = { - DEFAULT_DOMAIN: "wss://prod.vertexprotocol-backend.com", - TESTNET_DOMAIN: "wss://test.vertexprotocol-backend.com", + DEFAULT_DOMAIN: "wss://gateway.prod.vertexprotocol.com/v1/ws", + TESTNET_DOMAIN: "wss://gateway.sepolia-test.vertexprotocol.com/v1/ws", +} + +ARCHIVE_INDEXER_URLS = { + DEFAULT_DOMAIN: "https://archive.prod.vertexprotocol.com/v1", + TESTNET_DOMAIN: "https://archive.sepolia-test.vertexprotocol.com/v1", +} + +WS_SUBSCRIBE_URLS = { + DEFAULT_DOMAIN: "wss://gateway.prod.vertexprotocol.com/v1/subscribe", + TESTNET_DOMAIN: "wss://gateway.vertexprotocol-vertexprotocol.com/v1/subscribe", } CONTRACTS = { @@ -55,8 +65,6 @@ QUERY_PATH_URL = "/query" INDEXER_PATH_URL = "/indexer" SYMBOLS_PATH_URL = "/symbols" -WS_PATH_URL = "/ws" -WS_SUBSCRIBE_PATH_URL = "/subscribe" # POST METHODS PLACE_ORDER_METHOD = "place_order" diff --git a/hummingbot/connector/exchange/vertex/vertex_exchange.py b/hummingbot/connector/exchange/vertex/vertex_exchange.py index 4250eab3c5..25a379ebb7 100644 --- a/hummingbot/connector/exchange/vertex/vertex_exchange.py +++ b/hummingbot/connector/exchange/vertex/vertex_exchange.py @@ -733,7 +733,7 @@ async def _get_last_traded_price(self, trading_pair: str) -> float: matches_response = await self._api_post( path_url=CONSTANTS.INDEXER_PATH_URL, data=data, - limit_id=CONSTANTS.INDEXER_PATH_URL, + limit_id=CONSTANTS.INDEXER_PATH_URL ) matches = matches_response.get("matches", []) if matches and len(matches) > 0: diff --git a/hummingbot/connector/exchange/vertex/vertex_web_utils.py b/hummingbot/connector/exchange/vertex/vertex_web_utils.py index 9e55023dea..f0f8bb3700 100644 --- a/hummingbot/connector/exchange/vertex/vertex_web_utils.py +++ b/hummingbot/connector/exchange/vertex/vertex_web_utils.py @@ -15,7 +15,10 @@ def public_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> st :return: the full URL to the endpoint """ - return CONSTANTS.BASE_URLS[domain] + path_url + if "/indexer" in path_url: + return CONSTANTS.ARCHIVE_INDEXER_URLS[domain] + else: + return CONSTANTS.BASE_URLS[domain] + path_url def private_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> str: diff --git a/hummingbot/connector/markets_recorder.py b/hummingbot/connector/markets_recorder.py index e26324ab48..af929ebe5a 100644 --- a/hummingbot/connector/markets_recorder.py +++ b/hummingbot/connector/markets_recorder.py @@ -35,6 +35,7 @@ SellOrderCreatedEvent, ) from hummingbot.logger import HummingbotLogger +from hummingbot.model.controllers import Controllers from hummingbot.model.executors import Executors from hummingbot.model.funding_payment import FundingPayment from hummingbot.model.market_data import MarketData @@ -45,6 +46,7 @@ from hummingbot.model.range_position_update import RangePositionUpdate from hummingbot.model.sql_connection_manager import SQLConnectionManager from hummingbot.model.trade_fill import TradeFill +from hummingbot.smart_components.controllers.controller_base import ControllerConfigBase from hummingbot.smart_components.models.executors_info import ExecutorInfo @@ -205,6 +207,17 @@ def store_or_update_executor(self, executor): session.add(new_executor) session.commit() + def store_controller_config(self, controller_config: ControllerConfigBase): + with self._sql_manager.get_new_session() as session: + config = json.loads(controller_config.json()) + base_columns = ["id", "timestamp", "type"] + controller = Controllers(id=config["id"], + timestamp=time.time(), + type=config["controller_type"], + config={k: v for k, v in config.items() if k not in base_columns}) + session.add(controller) + session.commit() + def get_executors_by_ids(self, executor_ids: List[str]): with self._sql_manager.get_new_session() as session: executors = session.query(Executors).filter(Executors.id.in_(executor_ids)).all() diff --git a/hummingbot/core/data_type/in_flight_order.py b/hummingbot/core/data_type/in_flight_order.py index 7362e17840..23da7c20e8 100644 --- a/hummingbot/core/data_type/in_flight_order.py +++ b/hummingbot/core/data_type/in_flight_order.py @@ -304,13 +304,12 @@ def cumulative_fee_paid(self, token: str, exchange: Optional['ExchangeBase'] = N total_fee_in_token = Decimal("0") for trade_update in self.order_fills.values(): total_fee_in_token += trade_update.fee.fee_amount_in_token( - trading_pair=trade_update.trading_pair, + trading_pair=self.trading_pair, price=trade_update.fill_price, order_amount=trade_update.fill_base_amount, token=token, exchange=exchange ) - return total_fee_in_token def update_with_order_update(self, order_update: OrderUpdate) -> bool: diff --git a/hummingbot/core/data_type/trade_fee.py b/hummingbot/core/data_type/trade_fee.py index c1fcc0103c..94f01f49be 100644 --- a/hummingbot/core/data_type/trade_fee.py +++ b/hummingbot/core/data_type/trade_fee.py @@ -210,9 +210,8 @@ def fee_amount_in_token( if self._are_tokens_interchangeable(quote, token): fee_amount += amount_from_percentage else: - conversion_pair: str = combine_to_hb_trading_pair(base=quote, quote=token) - conversion_rate: Decimal = self._get_exchange_rate(conversion_pair, exchange, rate_source) - fee_amount += amount_from_percentage * conversion_rate + conversion_rate: Decimal = self._get_exchange_rate(trading_pair, exchange, rate_source) + fee_amount += amount_from_percentage / conversion_rate for flat_fee in self.flat_fees: if self._are_tokens_interchangeable(flat_fee.token, token): # No need to convert the value diff --git a/hummingbot/core/mock_api/mock_web_server.py b/hummingbot/core/mock_api/mock_web_server.py index ca00c5bf55..57d25783a4 100644 --- a/hummingbot/core/mock_api/mock_web_server.py +++ b/hummingbot/core/mock_api/mock_web_server.py @@ -35,7 +35,7 @@ class MockWebServer: ---------- __instance : Humming Web App instance _ev_loop : event loops run asynchronous task - _impl : web applicaiton + _impl : web application _runner : web runner _started : if started indicator _stock_responses : stocked web response diff --git a/hummingbot/core/rate_oracle/rate_oracle.py b/hummingbot/core/rate_oracle/rate_oracle.py index 61fe5ed060..ea8291b8cb 100644 --- a/hummingbot/core/rate_oracle/rate_oracle.py +++ b/hummingbot/core/rate_oracle/rate_oracle.py @@ -9,8 +9,10 @@ from hummingbot.core.network_iterator import NetworkStatus from hummingbot.core.rate_oracle.sources.ascend_ex_rate_source import AscendExRateSource from hummingbot.core.rate_oracle.sources.binance_rate_source import BinanceRateSource +from hummingbot.core.rate_oracle.sources.binance_us_rate_source import BinanceUSRateSource from hummingbot.core.rate_oracle.sources.coin_cap_rate_source import CoinCapRateSource from hummingbot.core.rate_oracle.sources.coin_gecko_rate_source import CoinGeckoRateSource +from hummingbot.core.rate_oracle.sources.coinbase_advanced_trade_rate_source import CoinbaseAdvancedTradeRateSource from hummingbot.core.rate_oracle.sources.cube_rate_source import CubeRateSource from hummingbot.core.rate_oracle.sources.gate_io_rate_source import GateIoRateSource from hummingbot.core.rate_oracle.sources.kucoin_rate_source import KucoinRateSource @@ -21,11 +23,13 @@ RATE_ORACLE_SOURCES = { "binance": BinanceRateSource, + "binance_us": BinanceUSRateSource, "coin_gecko": CoinGeckoRateSource, "coin_cap": CoinCapRateSource, "kucoin": KucoinRateSource, "ascend_ex": AscendExRateSource, "gate_io": GateIoRateSource, + "coinbase_advanced_trade": CoinbaseAdvancedTradeRateSource, "cube": CubeRateSource, } diff --git a/hummingbot/core/rate_oracle/sources/binance_rate_source.py b/hummingbot/core/rate_oracle/sources/binance_rate_source.py index 5aa65805c4..f3b89ea8c3 100644 --- a/hummingbot/core/rate_oracle/sources/binance_rate_source.py +++ b/hummingbot/core/rate_oracle/sources/binance_rate_source.py @@ -14,7 +14,6 @@ class BinanceRateSource(RateSourceBase): def __init__(self): super().__init__() self._binance_exchange: Optional[BinanceExchange] = None # delayed because of circular reference - self._binance_us_exchange: Optional[BinanceExchange] = None # delayed because of circular reference @property def name(self) -> str: @@ -26,7 +25,6 @@ async def get_prices(self, quote_token: Optional[str] = None) -> Dict[str, Decim results = {} tasks = [ self._get_binance_prices(exchange=self._binance_exchange), - self._get_binance_prices(exchange=self._binance_us_exchange, quote_token="USD"), ] task_results = await safe_gather(*tasks, return_exceptions=True) for task_result in task_results: @@ -43,7 +41,6 @@ async def get_prices(self, quote_token: Optional[str] = None) -> Dict[str, Decim def _ensure_exchanges(self): if self._binance_exchange is None: self._binance_exchange = self._build_binance_connector_without_private_keys(domain="com") - self._binance_us_exchange = self._build_binance_connector_without_private_keys(domain="us") @staticmethod async def _get_binance_prices(exchange: 'BinanceExchange', quote_token: str = None) -> Dict[str, Decimal]: diff --git a/hummingbot/core/rate_oracle/sources/binance_us_rate_source.py b/hummingbot/core/rate_oracle/sources/binance_us_rate_source.py new file mode 100644 index 0000000000..906d119854 --- /dev/null +++ b/hummingbot/core/rate_oracle/sources/binance_us_rate_source.py @@ -0,0 +1,87 @@ +from decimal import Decimal +from typing import TYPE_CHECKING, Dict, Optional + +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.rate_oracle.sources.rate_source_base import RateSourceBase +from hummingbot.core.utils import async_ttl_cache +from hummingbot.core.utils.async_utils import safe_gather + +if TYPE_CHECKING: + from hummingbot.connector.exchange.binance.binance_exchange import BinanceExchange + + +class BinanceUSRateSource(RateSourceBase): + def __init__(self): + super().__init__() + self._binance_us_exchange: Optional[BinanceExchange] = None # delayed because of circular reference + + @property + def name(self) -> str: + return "binance_us" + + @async_ttl_cache(ttl=30, maxsize=1) + async def get_prices(self, quote_token: Optional[str] = None) -> Dict[str, Decimal]: + self._ensure_exchanges() + results = {} + tasks = [ + self._get_binance_prices(exchange=self._binance_us_exchange, quote_token="USD"), + ] + task_results = await safe_gather(*tasks, return_exceptions=True) + for task_result in task_results: + if isinstance(task_result, Exception): + self.logger().error( + msg="Unexpected error while retrieving rates from Binance. Check the log file for more info.", + exc_info=task_result, + ) + break + else: + results.update(task_result) + return results + + def _ensure_exchanges(self): + if self._binance_us_exchange is None: + self._binance_us_exchange = self._build_binance_connector_without_private_keys(domain="us") + + @staticmethod + async def _get_binance_prices(exchange: 'BinanceExchange', quote_token: str = None) -> Dict[str, Decimal]: + """ + Fetches binance prices + + :param exchange: The exchange instance from which to query prices. + :param quote_token: A quote symbol, if specified only pairs with the quote symbol are included for prices + :return: A dictionary of trading pairs and prices + """ + pairs_prices = await exchange.get_all_pairs_prices() + results = {} + for pair_price in pairs_prices: + try: + trading_pair = await exchange.trading_pair_associated_to_exchange_symbol(symbol=pair_price["symbol"]) + except KeyError: + continue # skip pairs that we don't track + if quote_token is not None: + base, quote = split_hb_trading_pair(trading_pair=trading_pair) + if quote != quote_token: + continue + bid_price = pair_price.get("bidPrice") + ask_price = pair_price.get("askPrice") + if bid_price is not None and ask_price is not None and 0 < Decimal(bid_price) <= Decimal(ask_price): + results[trading_pair] = (Decimal(bid_price) + Decimal(ask_price)) / Decimal("2") + + return results + + @staticmethod + def _build_binance_connector_without_private_keys(domain: str) -> 'BinanceExchange': + from hummingbot.client.hummingbot_application import HummingbotApplication + from hummingbot.connector.exchange.binance.binance_exchange import BinanceExchange + + app = HummingbotApplication.main_application() + client_config_map = app.client_config_map + + return BinanceExchange( + client_config_map=client_config_map, + binance_api_key="", + binance_api_secret="", + trading_pairs=[], + trading_required=False, + domain=domain, + ) diff --git a/hummingbot/core/rate_oracle/sources/coinbase_advanced_trade_rate_source.py b/hummingbot/core/rate_oracle/sources/coinbase_advanced_trade_rate_source.py new file mode 100644 index 0000000000..a4bd006371 --- /dev/null +++ b/hummingbot/core/rate_oracle/sources/coinbase_advanced_trade_rate_source.py @@ -0,0 +1,83 @@ +from decimal import Decimal +from typing import TYPE_CHECKING, Dict + +from hummingbot.connector.exchange.coinbase_advanced_trade.coinbase_advanced_trade_constants import DEFAULT_DOMAIN +from hummingbot.core.rate_oracle.sources.rate_source_base import RateSourceBase +from hummingbot.core.utils import async_ttl_cache +from hummingbot.core.utils.async_utils import safe_gather + +if TYPE_CHECKING: + from hummingbot.connector.exchange.coinbase_advanced_trade.coinbase_advanced_trade_exchange import ( + CoinbaseAdvancedTradeExchange, + ) + + +class CoinbaseAdvancedTradeRateSource(RateSourceBase): + def __init__(self): + super().__init__() + self._coinbase_exchange: CoinbaseAdvancedTradeExchange | None = None # delayed because of circular reference + + @property + def name(self) -> str: + return "coinbase_advanced_trade" + + @async_ttl_cache(ttl=30, maxsize=1) + async def get_prices(self, quote_token: str | None = None) -> Dict[str, Decimal]: + if quote_token is None: + quote_token = "USD" + + self._ensure_exchanges() + results = {} + tasks = [ + self._get_coinbase_prices(exchange=self._coinbase_exchange, quote_token=quote_token), + ] + task_results = await safe_gather(*tasks, return_exceptions=True) + for task_result in task_results: + if isinstance(task_result, Exception): + self.logger().error( + msg="Unexpected error while retrieving rates from Coinbase. Check the log file for more info.", + exc_info=task_result, + ) + break + else: + results |= {f"{k}-{quote_token}": v for k, v in task_result.items()} + return results + + def _ensure_exchanges(self): + if self._coinbase_exchange is None: + self._coinbase_exchange = self._build_coinbase_connector_without_private_keys(domain="com") + + async def _get_coinbase_prices( + self, + exchange: 'CoinbaseAdvancedTradeExchange', + quote_token: str = None) -> Dict[str, Decimal]: + """ + Fetches coinbase prices + + :param exchange: The exchange instance from which to query prices. + :param quote_token: A quote symbol, if specified only pairs with the quote symbol are included for prices + :return: A dictionary of trading pairs and prices + """ + token_price: Dict[str, str] = await exchange.get_exchange_rates(quote_token=quote_token) + self.logger().debug(f"retrieved {len(token_price)} prices for {quote_token}") + self.logger().debug(f" {token_price.get('ATOM')} {quote_token} for 1 ATOM") + return {token: Decimal(1.0) / Decimal(price) for token, price in token_price.items() if Decimal(price) != 0} + + @staticmethod + def _build_coinbase_connector_without_private_keys(domain: str = DEFAULT_DOMAIN) -> 'CoinbaseAdvancedTradeExchange': + from hummingbot.client.hummingbot_application import HummingbotApplication + from hummingbot.connector.exchange.coinbase_advanced_trade.coinbase_advanced_trade_exchange import ( + CoinbaseAdvancedTradeExchange, + ) + + app = HummingbotApplication.main_application() + client_config_map = app.client_config_map + + return CoinbaseAdvancedTradeExchange( + client_config_map=client_config_map, + coinbase_advanced_trade_api_key="", + coinbase_advanced_trade_api_secret="", + trading_pairs=[], + trading_required=False, + domain=domain, + ) diff --git a/hummingbot/model/controllers.py b/hummingbot/model/controllers.py new file mode 100644 index 0000000000..3fb447e235 --- /dev/null +++ b/hummingbot/model/controllers.py @@ -0,0 +1,16 @@ +from sqlalchemy import JSON, Column, Float, Index, Integer, Text + +from hummingbot.model import HummingbotBase + + +class Controllers(HummingbotBase): + __tablename__ = "Controllers" + __table_args__ = ( + Index("c_type", "type"), + ) + + id = Column(Text, primary_key=False) + controller_id = Column(Integer, primary_key=True, autoincrement=True) + timestamp = Column(Float, nullable=False) + type = Column(Text, nullable=False) + config = Column(JSON, nullable=False) diff --git a/hummingbot/smart_components/controllers/market_making_controller_base.py b/hummingbot/smart_components/controllers/market_making_controller_base.py index 2a303103a7..e47b5eb7f9 100644 --- a/hummingbot/smart_components/controllers/market_making_controller_base.py +++ b/hummingbot/smart_components/controllers/market_making_controller_base.py @@ -9,6 +9,7 @@ from hummingbot.smart_components.controllers.controller_base import ControllerBase, ControllerConfigBase from hummingbot.smart_components.executors.position_executor.data_types import TrailingStop, TripleBarrierConfig from hummingbot.smart_components.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction +from hummingbot.smart_components.models.executors import CloseType class MarketMakingControllerConfigBase(ControllerConfigBase): @@ -254,7 +255,7 @@ def create_actions_proposal(self) -> List[ExecutorAction]: def get_levels_to_execute(self) -> List[str]: working_levels = self.filter_executors( executors=self.executors_info, - filter_func=lambda x: x.is_active or (x.is_done and x.filled_amount_quote > Decimal("0") and time.time() - x.close_timestamp < self.config.cooldown_time) + filter_func=lambda x: x.is_active or (x.close_type == CloseType.STOP_LOSS and x.is_done and x.filled_amount_quote > Decimal("0") and time.time() - x.close_timestamp < self.config.cooldown_time) ) working_levels_ids = [executor.custom_info["level_id"] for executor in working_levels] return self.get_not_active_levels_ids(working_levels_ids) diff --git a/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py index 29bcbc7844..1e8863d133 100644 --- a/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py +++ b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py @@ -1,10 +1,8 @@ import asyncio import logging from decimal import Decimal -from functools import lru_cache from typing import Union -from hummingbot.client.settings import AllConnectorSettings from hummingbot.connector.utils import split_hb_trading_pair from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.event.events import BuyOrderCreatedEvent, MarketOrderFailureEvent, SellOrderCreatedEvent @@ -28,13 +26,6 @@ def logger(cls) -> HummingbotLogger: cls._logger = logging.getLogger(__name__) return cls._logger - @staticmethod - @lru_cache(maxsize=10) - def is_amm(exchange: str) -> bool: - return exchange in sorted( - AllConnectorSettings.get_gateway_amm_connector_names() - ) - @property def is_closed(self): return self.arbitrage_status in [ArbitrageExecutorStatus.COMPLETED, ArbitrageExecutorStatus.FAILED] @@ -217,7 +208,7 @@ async def get_trade_pnl_pct(self): async def get_tx_cost_in_asset(self, exchange: str, trading_pair: str, is_buy: bool, order_amount: Decimal, asset: str): connector = self.connectors[exchange] price = await self.get_resulting_price_for_amount(exchange, trading_pair, is_buy, order_amount) - if self.is_amm(exchange=exchange): + if self.is_amm_connector(exchange=exchange): gas_cost = connector.network_transaction_fee conversion_price = RateOracle.get_instance().get_pair_rate(f"{asset}-{gas_cost.token}") return gas_cost.amount / conversion_price diff --git a/hummingbot/smart_components/executors/arbitrage_executor/data_types.py b/hummingbot/smart_components/executors/arbitrage_executor/data_types.py index 353f0ab2f9..f3688dfb36 100644 --- a/hummingbot/smart_components/executors/arbitrage_executor/data_types.py +++ b/hummingbot/smart_components/executors/arbitrage_executor/data_types.py @@ -1,20 +1,13 @@ from decimal import Decimal from enum import Enum -from pydantic import BaseModel - -from hummingbot.smart_components.executors.data_types import ExecutorConfigBase - - -class ExchangePair(BaseModel): - connector_name: str - trading_pair: str +from hummingbot.smart_components.executors.data_types import ConnectorPair, ExecutorConfigBase class ArbitrageExecutorConfig(ExecutorConfigBase): type = "arbitrage_executor" - buying_market: ExchangePair - selling_market: ExchangePair + buying_market: ConnectorPair + selling_market: ConnectorPair order_amount: Decimal min_profitability: Decimal max_retries: int = 3 diff --git a/hummingbot/smart_components/executors/data_types.py b/hummingbot/smart_components/executors/data_types.py index cd38df0e31..2128846e30 100644 --- a/hummingbot/smart_components/executors/data_types.py +++ b/hummingbot/smart_components/executors/data_types.py @@ -22,3 +22,8 @@ def set_id(cls, v, values): hashed_id = hashlib.sha256(raw_id.encode()).digest() # Get bytes return base58.b58encode(hashed_id).decode() # Base58 encode return v + + +class ConnectorPair(BaseModel): + connector_name: str + trading_pair: str diff --git a/hummingbot/smart_components/executors/dca_executor/dca_executor.py b/hummingbot/smart_components/executors/dca_executor/dca_executor.py index f29c3f6a49..3e2b8ac316 100644 --- a/hummingbot/smart_components/executors/dca_executor/dca_executor.py +++ b/hummingbot/smart_components/executors/dca_executor/dca_executor.py @@ -435,12 +435,26 @@ async def control_shutdown_process(self): if math.isclose(self.open_filled_amount, self.close_filled_amount): self.close_execution_by(self.close_type) elif len(self.active_close_orders) > 0: - self.logger().info(f"Waiting for close order {self.active_close_orders[0].order_id} to be filled | Open amount: {self.open_filled_amount}, Close amount: {self.close_filled_amount}") + connector = self.connectors[self.config.connector_name] + await connector._update_orders_with_error_handler( + orders=[order.order for order in self.active_close_orders if order.order], + error_handler=connector._handle_update_error_for_active_order + ) + for order in self.active_close_orders: + self.update_tracked_orders_with_order_id(order.order_id) + if order.order and order.order.is_done and order.executed_amount_base == Decimal("0"): + self.logger().error( + f"Close order {order.order_id} is done, might be an error with this update. Cancelling the order and placing it again.") + self._strategy.cancel(connector_name=self.config.connector_name, trading_pair=self.config.trading_pair, + order_id=order.order_id) + self._close_orders.remove(order) + self._failed_orders.append(order) else: - self.logger().info(f"Open amount: {self.open_filled_amount}, Close amount: {self.close_filled_amount}, Back up filled amount {self._total_executed_amount_backup}") + self.logger().info( + f"Open amount: {self.open_filled_amount}, Close amount: {self.close_filled_amount}, Back up filled amount {self._total_executed_amount_backup}") self.place_close_order_and_cancel_open_orders() self._current_retries += 1 - await asyncio.sleep(1.0) + await asyncio.sleep(5.0) def update_tracked_orders_with_order_id(self, order_id: str): all_orders = self._open_orders + self._close_orders @@ -520,4 +534,5 @@ def get_custom_info(self) -> Dict: "current_retries": self._current_retries, "max_retries": self._max_retries, "level_id": self.config.level_id, + "order_ids": [order.order_id for order in self._open_orders + self._close_orders], } diff --git a/hummingbot/smart_components/executors/executor_base.py b/hummingbot/smart_components/executors/executor_base.py index ebf4f5ce80..cf567e51cc 100644 --- a/hummingbot/smart_components/executors/executor_base.py +++ b/hummingbot/smart_components/executors/executor_base.py @@ -1,6 +1,8 @@ from decimal import Decimal +from functools import lru_cache from typing import Dict, List, Optional, Tuple, Union +from hummingbot.client.settings import AllConnectorSettings from hummingbot.connector.connector_base import ConnectorBase from hummingbot.connector.trading_rule import TradingRule from hummingbot.core.data_type.common import OrderType, PositionAction, PriceType, TradeType @@ -140,6 +142,13 @@ def is_perpetual_connector(connector_name: str): """ return "perpetual" in connector_name.lower() + @staticmethod + @lru_cache(maxsize=10) + def is_amm_connector(exchange: str) -> bool: + return exchange in sorted( + AllConnectorSettings.get_gateway_amm_connector_names() + ) + def start(self): """ Starts the executor and registers the events. diff --git a/hummingbot/smart_components/executors/executor_orchestrator.py b/hummingbot/smart_components/executors/executor_orchestrator.py index de72a49e2d..a1ffb15a49 100644 --- a/hummingbot/smart_components/executors/executor_orchestrator.py +++ b/hummingbot/smart_components/executors/executor_orchestrator.py @@ -12,6 +12,8 @@ from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor from hummingbot.smart_components.executors.twap_executor.data_types import TWAPExecutorConfig from hummingbot.smart_components.executors.twap_executor.twap_executor import TWAPExecutor +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.smart_components.executors.xemm_executor.xemm_executor import XEMMExecutor from hummingbot.smart_components.models.executor_actions import ( CreateExecutorAction, ExecutorAction, @@ -94,6 +96,8 @@ def create_executor(self, action: CreateExecutorAction): executor = ArbitrageExecutor(self.strategy, executor_config, self.executors_update_interval) elif isinstance(executor_config, TWAPExecutorConfig): executor = TWAPExecutor(self.strategy, executor_config, self.executors_update_interval) + elif isinstance(executor_config, XEMMExecutorConfig): + executor = XEMMExecutor(self.strategy, executor_config, self.executors_update_interval) else: raise ValueError("Unsupported executor config type") diff --git a/hummingbot/smart_components/executors/xemm_executor/__init__.py b/hummingbot/smart_components/executors/xemm_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/smart_components/executors/xemm_executor/data_types.py b/hummingbot/smart_components/executors/xemm_executor/data_types.py new file mode 100644 index 0000000000..f5e1f22c72 --- /dev/null +++ b/hummingbot/smart_components/executors/xemm_executor/data_types.py @@ -0,0 +1,15 @@ +from decimal import Decimal + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.data_types import ConnectorPair, ExecutorConfigBase + + +class XEMMExecutorConfig(ExecutorConfigBase): + type = "xemm_executor" + buying_market: ConnectorPair + selling_market: ConnectorPair + maker_side: TradeType + order_amount: Decimal + min_profitability: Decimal + target_profitability: Decimal + max_profitability: Decimal diff --git a/hummingbot/smart_components/executors/xemm_executor/xemm_executor.py b/hummingbot/smart_components/executors/xemm_executor/xemm_executor.py new file mode 100644 index 0000000000..c69d31ef76 --- /dev/null +++ b/hummingbot/smart_components/executors/xemm_executor/xemm_executor.py @@ -0,0 +1,322 @@ +import logging +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, Union +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.data_type.common import OrderType, PriceType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketOrderFailureEvent, + SellOrderCompletedEvent, + SellOrderCreatedEvent, +) +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.logger import HummingbotLogger +from hummingbot.smart_components.executors.executor_base import ExecutorBase +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.smart_components.models.base import SmartComponentStatus +from hummingbot.smart_components.models.executors import CloseType, TrackedOrder +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class XEMMExecutor(ExecutorBase): + _logger = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls._logger is None: + cls._logger = logging.getLogger(__name__) + return cls._logger + + @staticmethod + def _are_tokens_interchangeable(first_token: str, second_token: str): + interchangeable_tokens = [ + {"WETH", "ETH"}, + {"WBTC", "BTC"}, + {"WBNB", "BNB"}, + {"WMATIC", "MATIC"}, + {"WAVAX", "AVAX"}, + {"WONE", "ONE"}, + ] + same_token_condition = first_token == second_token + tokens_interchangeable_condition = any(({first_token, second_token} <= interchangeable_pair + for interchangeable_pair + in interchangeable_tokens)) + # for now, we will consider all the stablecoins interchangeable + stable_coins_condition = "USD" in first_token and "USD" in second_token + return same_token_condition or tokens_interchangeable_condition or stable_coins_condition + + def is_arbitrage_valid(self, pair1, pair2): + base_asset1, quote_asset1 = split_hb_trading_pair(pair1) + base_asset2, quote_asset2 = split_hb_trading_pair(pair2) + return self._are_tokens_interchangeable(base_asset1, base_asset2) and \ + self._are_tokens_interchangeable(quote_asset1, quote_asset2) + + def __init__(self, strategy: ScriptStrategyBase, config: XEMMExecutorConfig, update_interval: float = 1.0, + max_retries: int = 10): + if not self.is_arbitrage_valid(pair1=config.buying_market.trading_pair, + pair2=config.selling_market.trading_pair): + raise Exception("XEMM is not valid since the trading pairs are not interchangeable.") + self.config = config + if config.maker_side == TradeType.BUY: + self.maker_connector = config.buying_market.connector_name + self.maker_trading_pair = config.buying_market.trading_pair + self.maker_order_side = TradeType.BUY + self.taker_connector = config.selling_market.connector_name + self.taker_trading_pair = config.selling_market.trading_pair + self.taker_order_side = TradeType.SELL + else: + self.maker_connector = config.selling_market.connector_name + self.maker_trading_pair = config.selling_market.trading_pair + self.maker_order_side = TradeType.SELL + self.taker_connector = config.buying_market.connector_name + self.taker_trading_pair = config.buying_market.trading_pair + self.taker_order_side = TradeType.BUY + taker_connector = strategy.connectors[self.taker_connector] + if not self.is_amm_connector(exchange=self.taker_connector): + if OrderType.MARKET not in taker_connector.supported_order_types(): + raise ValueError(f"{self.taker_connector} does not support market orders.") + self._taker_result_price = Decimal("1") + self._maker_target_price = Decimal("1") + self._tx_cost = Decimal("1") + self._tx_cost_pct = Decimal("1") + self.maker_order = None + self.taker_order = None + self.failed_orders = [] + self._current_retries = 0 + self._max_retries = max_retries + super().__init__(strategy=strategy, + connectors=[config.buying_market.connector_name, config.selling_market.connector_name], + config=config, update_interval=update_interval) + + def validate_sufficient_balance(self): + mid_price = self.get_price(self.maker_connector, self.maker_trading_pair, + price_type=PriceType.MidPrice) + maker_order_candidate = OrderCandidate( + trading_pair=self.maker_trading_pair, + is_maker=True, + order_type=OrderType.LIMIT, + order_side=self.maker_order_side, + amount=self.config.order_amount, + price=mid_price,) + taker_order_candidate = OrderCandidate( + trading_pair=self.taker_trading_pair, + is_maker=False, + order_type=OrderType.MARKET, + order_side=self.taker_order_side, + amount=self.config.order_amount, + price=mid_price,) + maker_adjusted_candidate = self.adjust_order_candidates(self.maker_connector, [maker_order_candidate])[0] + taker_adjusted_candidate = self.adjust_order_candidates(self.taker_connector, [taker_order_candidate])[0] + if maker_adjusted_candidate.amount == Decimal("0") or taker_adjusted_candidate.amount == Decimal("0"): + self.close_type = CloseType.INSUFFICIENT_BALANCE + self.logger().error("Not enough budget to open position.") + self.stop() + + async def control_task(self): + if self.status == SmartComponentStatus.RUNNING: + await self.update_prices_and_tx_costs() + await self.control_maker_order() + elif self.status == SmartComponentStatus.SHUTTING_DOWN: + await self.control_shutdown_process() + + async def control_maker_order(self): + if self.maker_order is None: + await self.create_maker_order() + else: + await self.control_update_maker_order() + + async def update_prices_and_tx_costs(self): + self._taker_result_price = await self.get_resulting_price_for_amount( + connector=self.taker_connector, + trading_pair=self.taker_trading_pair, + is_buy=self.taker_order_side == TradeType.BUY, + order_amount=self.config.order_amount) + self._tx_cost = await self.get_tx_cost() + self._tx_cost_pct = self._tx_cost / self.config.order_amount + if self.taker_order_side == TradeType.BUY: + self._maker_target_price = self._taker_result_price * (1 + self.config.target_profitability + self._tx_cost_pct) + else: + self._maker_target_price = self._taker_result_price * (1 - self.config.target_profitability - self._tx_cost_pct) + + async def get_tx_cost(self): + base, quote = split_hb_trading_pair(trading_pair=self.config.buying_market.trading_pair) + # TODO: also due the fact that we don't have a good rate oracle source we have to use a fixed token + base_without_wrapped = base[1:] if base.startswith("W") else base + taker_fee = await self.get_tx_cost_in_asset( + exchange=self.taker_connector, + trading_pair=self.taker_trading_pair, + order_type=OrderType.MARKET, + is_buy=True, + order_amount=self.config.order_amount, + asset=base_without_wrapped + ) + maker_fee = await self.get_tx_cost_in_asset( + exchange=self.maker_connector, + trading_pair=self.maker_trading_pair, + order_type=OrderType.LIMIT, + is_buy=False, + order_amount=self.config.order_amount, + asset=base_without_wrapped) + return taker_fee + maker_fee + + async def get_tx_cost_in_asset(self, exchange: str, trading_pair: str, is_buy: bool, order_amount: Decimal, + asset: str, order_type: OrderType = OrderType.MARKET): + connector = self.connectors[exchange] + if self.is_amm_connector(exchange=exchange): + gas_cost = connector.network_transaction_fee + conversion_price = RateOracle.get_instance().get_pair_rate(f"{asset}-{gas_cost.token}") + return gas_cost.amount / conversion_price + else: + fee = connector.get_fee( + base_currency=asset, + quote_currency=trading_pair.split("-")[1], + order_type=order_type, + order_side=TradeType.BUY if is_buy else TradeType.SELL, + amount=order_amount, + price=self._taker_result_price, + is_maker=False + ) + return fee.fee_amount_in_token( + trading_pair=trading_pair, + price=self._taker_result_price, + order_amount=order_amount, + token=asset, + exchange=connector, + ) + + async def get_resulting_price_for_amount(self, connector: str, trading_pair: str, is_buy: bool, + order_amount: Decimal): + return await self.connectors[connector].get_quote_price(trading_pair, is_buy, order_amount) + + async def create_maker_order(self): + order_id = self.place_order( + connector_name=self.maker_connector, + trading_pair=self.maker_trading_pair, + order_type=OrderType.LIMIT, + side=self.maker_order_side, + amount=self.config.order_amount, + price=self._maker_target_price) + self.maker_order = TrackedOrder(order_id=order_id) + self.logger().info(f"Created maker order {order_id} at price {self._maker_target_price}.") + + async def control_shutdown_process(self): + if self.maker_order.is_done and self.taker_order.is_done: + self.logger().info("Both orders are done, executor terminated.") + self.stop() + + async def control_update_maker_order(self): + trade_profitability = self.get_current_trade_profitability() + if trade_profitability - self._tx_cost_pct < self.config.min_profitability: + self.logger().info(f"Trade profitability {trade_profitability - self._tx_cost_pct} is below minimum profitability. Cancelling order.") + self._strategy.cancel(self.maker_connector, self.maker_trading_pair, self.maker_order.order_id) + self.maker_order = None + if trade_profitability - self._tx_cost_pct > self.config.max_profitability: + self.logger().info(f"Trade profitability {trade_profitability - self._tx_cost_pct} is above target profitability. Cancelling order.") + self._strategy.cancel(self.maker_connector, self.maker_trading_pair, self.maker_order.order_id) + self.maker_order = None + + def get_current_trade_profitability(self): + trade_profitability = Decimal("0") + if self.maker_order and self.maker_order.order and self.maker_order.order.is_open: + maker_price = self.maker_order.order.price + if self.maker_order_side == TradeType.BUY: + trade_profitability = (self._taker_result_price - maker_price) / maker_price + else: + trade_profitability = (maker_price - self._taker_result_price) / maker_price + return trade_profitability + + def process_order_created_event(self, + event_tag: int, + market: ConnectorBase, + event: Union[BuyOrderCreatedEvent, SellOrderCreatedEvent]): + if self.maker_order and event.order_id == self.maker_order.order_id: + self.logger().info(f"Maker order {event.order_id} created.") + self.maker_order.order = self.get_in_flight_order(self.maker_connector, event.order_id) + elif self.taker_order and event.order_id == self.taker_order.order_id: + self.logger().info(f"Taker order {event.order_id} created.") + self.taker_order.order = self.get_in_flight_order(self.taker_connector, event.order_id) + + def process_order_completed_event(self, + event_tag: int, + market: ConnectorBase, + event: Union[BuyOrderCompletedEvent, SellOrderCompletedEvent]): + if self.maker_order and event.order_id == self.maker_order.order_id: + self.logger().info(f"Maker order {event.order_id} completed. Executing taker order.") + self.place_taker_order() + self._status = SmartComponentStatus.SHUTTING_DOWN + + def place_taker_order(self): + taker_order_id = self.place_order( + connector_name=self.taker_connector, + trading_pair=self.taker_trading_pair, + order_type=OrderType.MARKET, + side=self.taker_order_side, + amount=self.config.order_amount) + self.taker_order = TrackedOrder(order_id=taker_order_id) + + def process_order_failed_event(self, _, market, event: MarketOrderFailureEvent): + if self.maker_order and self.maker_order.order_id == event.order_id: + self.failed_orders.append(self.maker_order) + self.maker_order = None + self._current_retries += 1 + elif self.taker_order and self.taker_order.order_id == event.order_id: + self.failed_orders.append(self.taker_order) + self._current_retries += 1 + self.place_taker_order() + + def get_custom_info(self) -> Dict: + trade_profitability = self.get_current_trade_profitability() + return { + "side": self.config.maker_side, + "maker_connector": self.maker_connector, + "maker_trading_pair": self.maker_trading_pair, + "taker_connector": self.taker_connector, + "taker_trading_pair": self.taker_trading_pair, + "min_profitability": self.config.min_profitability, + "target_profitability_pct": self.config.target_profitability, + "max_profitability": self.config.max_profitability, + "trade_profitability": trade_profitability, + "tx_cost": self._tx_cost, + "tx_cost_pct": self._tx_cost_pct, + } + + def early_stop(self): + if self.maker_order and self.maker_order.order and self.maker_order.order.is_open: + self.logger().info(f"Cancelling maker order {self.maker_order.order_id}.") + self._strategy.cancel(self.maker_connector, self.maker_trading_pair, self.maker_order.order_id) + self.close_type = CloseType.EARLY_STOP + self.stop() + + def get_cum_fees_quote(self) -> Decimal: + if self.is_closed and self.maker_order and self.taker_order: + return self.maker_order.cum_fees_quote + self.taker_order.cum_fees_quote + else: + return Decimal("0") + + def get_net_pnl_quote(self) -> Decimal: + if self.is_closed and self.maker_order and self.taker_order and self.maker_order.is_done and self.taker_order.is_done: + maker_pnl = self.maker_order.executed_amount_base * self.maker_order.average_executed_price + taker_pnl = self.taker_order.executed_amount_base * self.taker_order.average_executed_price + return taker_pnl - maker_pnl - self.get_cum_fees_quote() + else: + return Decimal("0") + + def get_net_pnl_pct(self) -> Decimal: + pnl_quote = self.get_net_pnl_quote() + return pnl_quote / self.config.order_amount + + def to_format_status(self): + trade_profitability = self.get_current_trade_profitability() + return f""" +Maker Side: {self.maker_order_side} +----------------------------------------------------------------------------------------------------------------------- + - Maker: {self.maker_connector} {self.maker_trading_pair} | Taker: {self.taker_connector} {self.taker_trading_pair} + - Min profitability: {self.config.min_profitability*100:.2f}% | Target profitability: {self.config.target_profitability*100:.2f}% | Max profitability: {self.config.max_profitability*100:.2f}% | Current profitability: {(trade_profitability - self._tx_cost_pct)*100:.2f}% + - Trade profitability: {trade_profitability*100:.2f}% | Tx cost: {self._tx_cost_pct*100:.2f}% + - Taker result price: {self._taker_result_price:.3f} | Tx cost: {self._tx_cost:.3f} {self.maker_trading_pair.split('-')[-1]} | Order amount (Base): {self.config.order_amount:.2f} +----------------------------------------------------------------------------------------------------------------------- +""" diff --git a/hummingbot/smart_components/models/executor_actions.py b/hummingbot/smart_components/models/executor_actions.py index 9e8b18241a..a9e1d263ed 100644 --- a/hummingbot/smart_components/models/executor_actions.py +++ b/hummingbot/smart_components/models/executor_actions.py @@ -6,6 +6,7 @@ from hummingbot.smart_components.executors.dca_executor.data_types import DCAExecutorConfig from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorConfig from hummingbot.smart_components.executors.twap_executor.data_types import TWAPExecutorConfig +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig class ExecutorAction(BaseModel): @@ -19,7 +20,7 @@ class CreateExecutorAction(ExecutorAction): """ Action to create an executor. """ - executor_config: Union[PositionExecutorConfig, DCAExecutorConfig, ArbitrageExecutorConfig, TWAPExecutorConfig] + executor_config: Union[PositionExecutorConfig, DCAExecutorConfig, XEMMExecutorConfig, ArbitrageExecutorConfig, TWAPExecutorConfig] class StopExecutorAction(ExecutorAction): diff --git a/hummingbot/smart_components/models/executors_info.py b/hummingbot/smart_components/models/executors_info.py index 983089c432..afa3fc6c25 100644 --- a/hummingbot/smart_components/models/executors_info.py +++ b/hummingbot/smart_components/models/executors_info.py @@ -9,6 +9,7 @@ from hummingbot.smart_components.executors.dca_executor.data_types import DCAExecutorConfig from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorConfig from hummingbot.smart_components.executors.twap_executor.data_types import TWAPExecutorConfig +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig from hummingbot.smart_components.models.base import SmartComponentStatus from hummingbot.smart_components.models.executors import CloseType @@ -20,7 +21,7 @@ class ExecutorInfo(BaseModel): close_timestamp: Optional[float] close_type: Optional[CloseType] status: SmartComponentStatus - config: Union[PositionExecutorConfig, ArbitrageExecutorConfig, DCAExecutorConfig, TWAPExecutorConfig, ExecutorConfigBase] + config: Union[PositionExecutorConfig, XEMMExecutorConfig, ArbitrageExecutorConfig, DCAExecutorConfig, TWAPExecutorConfig, ExecutorConfigBase] net_pnl_pct: Decimal net_pnl_quote: Decimal cum_fees_quote: Decimal @@ -46,6 +47,11 @@ def trading_pair(self) -> Optional[str]: def connector_name(self) -> Optional[str]: return self.config.connector_name + def to_dict(self): + base_dict = self.dict() + base_dict["side"] = self.side + return base_dict + class ExecutorHandlerInfo(BaseModel): controller_id: str diff --git a/hummingbot/strategy/strategy_v2_base.py b/hummingbot/strategy/strategy_v2_base.py index 729f84257f..5a6f690ae4 100644 --- a/hummingbot/strategy/strategy_v2_base.py +++ b/hummingbot/strategy/strategy_v2_base.py @@ -13,6 +13,7 @@ from hummingbot.client.config.config_data_types import BaseClientModel, ClientFieldData from hummingbot.client.ui.interface_utils import format_df_for_printout from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.connector.markets_recorder import MarketsRecorder from hummingbot.core.data_type.common import PositionMode from hummingbot.core.event.events import ExecutorEvent from hummingbot.core.pubsub import PubSub @@ -216,6 +217,7 @@ def initialize_controllers(self): controllers_configs = self.config.load_controller_configs() for controller_config in controllers_configs: self.add_controller(controller_config) + MarketsRecorder.get_instance().store_controller_config(controller_config) def add_controller(self, config: ControllerConfigBase): try: @@ -336,7 +338,7 @@ def executors_info_to_df(executors_info: List[ExecutorInfo]) -> pd.DataFrame: """ Convert a list of executor handler info to a dataframe. """ - df = pd.DataFrame([ei.dict() for ei in executors_info]) + df = pd.DataFrame([ei.to_dict() for ei in executors_info]) # Convert the enum values to integers df['status'] = df['status'].apply(lambda x: x.value) @@ -345,12 +347,11 @@ def executors_info_to_df(executors_info: List[ExecutorInfo]) -> pd.DataFrame: # Convert back to enums for display df['status'] = df['status'].apply(SmartComponentStatus) - return df[["id", "timestamp", "type", "status", "net_pnl_pct", "net_pnl_quote", "cum_fees_quote", "is_trading", - "filled_amount_quote", "close_type"]] + return df def format_status(self) -> str: original_info = super().format_status() - columns_to_show = ["id", "type", "status", "net_pnl_pct", "net_pnl_quote", "cum_fees_quote", + columns_to_show = ["type", "side", "status", "net_pnl_pct", "net_pnl_quote", "cum_fees_quote", "filled_amount_quote", "is_trading", "close_type", "age"] extra_info = [] @@ -389,13 +390,14 @@ def format_status(self) -> str: controller_performance_info.append("Close Types Count:") for close_type, count in performance_report.close_type_counts.items(): controller_performance_info.append(f" {close_type}: {count}") + extra_info.extend(controller_performance_info) # Aggregate global metrics and close type counts global_realized_pnl_quote += performance_report.realized_pnl_quote global_unrealized_pnl_quote += performance_report.unrealized_pnl_quote global_volume_traded += performance_report.volume_traded - global_close_type_counts.update(performance_report.close_type_counts) - extra_info.extend(controller_performance_info) + for close_type, value in performance_report.close_type_counts.items(): + global_close_type_counts[close_type] = global_close_type_counts.get(close_type, 0) + value main_executors_list = self.get_executors_by_controller("main") if len(main_executors_list) > 0: @@ -403,6 +405,13 @@ def format_status(self) -> str: main_executors_df = self.executors_info_to_df(main_executors_list) main_executors_df["age"] = self.current_timestamp - main_executors_df["timestamp"] extra_info.extend([format_df_for_printout(main_executors_df[columns_to_show], table_format="psql")]) + main_performance_report = self.executor_orchestrator.generate_performance_report("main") + # Aggregate global metrics and close type counts + global_realized_pnl_quote += main_performance_report.realized_pnl_quote + global_unrealized_pnl_quote += main_performance_report.unrealized_pnl_quote + global_volume_traded += main_performance_report.volume_traded + for close_type, value in main_performance_report.close_type_counts.items(): + global_close_type_counts[close_type] = global_close_type_counts.get(close_type, 0) + value # Calculate and append global performance metrics global_pnl_quote = global_realized_pnl_quote + global_unrealized_pnl_quote diff --git a/scripts/archived_scripts/examples_using_smart_components/arbitrage_with_smart_component.py b/scripts/archived_scripts/examples_using_smart_components/arbitrage_with_smart_component.py index 5309156d3c..492561e231 100644 --- a/scripts/archived_scripts/examples_using_smart_components/arbitrage_with_smart_component.py +++ b/scripts/archived_scripts/examples_using_smart_components/arbitrage_with_smart_component.py @@ -2,14 +2,15 @@ from hummingbot.core.rate_oracle.rate_oracle import RateOracle from hummingbot.smart_components.executors.arbitrage_executor.arbitrage_executor import ArbitrageExecutor -from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageExecutorConfig, ExchangePair +from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageExecutorConfig +from hummingbot.smart_components.executors.data_types import ConnectorPair from hummingbot.strategy.script_strategy_base import ScriptStrategyBase class ArbitrageWithSmartComponent(ScriptStrategyBase): # Parameters - exchange_pair_1 = ExchangePair(connector_name="binance", trading_pair="MATIC-USDT") - exchange_pair_2 = ExchangePair(connector_name="uniswap_polygon_mainnet", trading_pair="WMATIC-USDT") + exchange_pair_1 = ConnectorPair(connector_name="binance", trading_pair="MATIC-USDT") + exchange_pair_2 = ConnectorPair(connector_name="uniswap_polygon_mainnet", trading_pair="WMATIC-USDT") order_amount = Decimal("50") # in base asset min_profitability = Decimal("0.004") @@ -42,7 +43,7 @@ def on_stop(self): for arbitrage in self.active_sell_arbitrages: arbitrage.stop() - def create_arbitrage_executor(self, buying_exchange_pair: ExchangePair, selling_exchange_pair: ExchangePair): + def create_arbitrage_executor(self, buying_exchange_pair: ConnectorPair, selling_exchange_pair: ConnectorPair): try: base_asset_for_selling_exchange = self.connectors[selling_exchange_pair.exchange].get_available_balance( selling_exchange_pair.trading_pair.split("-")[0]) diff --git a/scripts/archived_scripts/examples_using_smart_components/directional_strategy_bb_rsi_multi_timeframe.py b/scripts/archived_scripts/examples_using_smart_components/directional_strategy_bb_rsi_multi_timeframe.py index 560318d071..23e2cc79d8 100644 --- a/scripts/archived_scripts/examples_using_smart_components/directional_strategy_bb_rsi_multi_timeframe.py +++ b/scripts/archived_scripts/examples_using_smart_components/directional_strategy_bb_rsi_multi_timeframe.py @@ -69,7 +69,7 @@ def get_signal(self): # -1 --> short | 1 --> long, so in the normalization we also need to switch side by changing the sign sma_rsi_normalized = -1 * (last_row["RSI_21_SMA_10"].item() - 50) / 50 bb_percentage_normalized = -1 * (last_row["BBP_21_2.0"].item() - 0.5) / 0.5 - # we assume that the weigths of sma of rsi and bb are equal + # we assume that the weights of sma of rsi and bb are equal signal_value = (sma_rsi_normalized + bb_percentage_normalized) / 2 signals.append(signal_value) # Here we have a list with the values of the signals for each candle diff --git a/scripts/funding_rate_arb.py b/scripts/funding_rate_arb.py new file mode 100644 index 0000000000..00f0714d5c --- /dev/null +++ b/scripts/funding_rate_arb.py @@ -0,0 +1,362 @@ +import os +from decimal import Decimal +from typing import Dict, List, Set + +import pandas as pd +from pydantic import Field, validator + +from hummingbot.client.config.config_data_types import ClientFieldData +from hummingbot.client.ui.interface_utils import format_df_for_printout +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.clock import Clock +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PriceType, TradeType +from hummingbot.core.event.events import FundingPaymentCompletedEvent +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.executors.position_executor.data_types import ( + PositionExecutorConfig, + TripleBarrierConfig, +) +from hummingbot.smart_components.models.executor_actions import CreateExecutorAction, StopExecutorAction +from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase + + +class FundingRateArbitrageConfig(StrategyV2ConfigBase): + script_file_name: str = Field(default_factory=lambda: os.path.basename(__file__)) + candles_config: List[CandlesConfig] = [] + controllers_config: List[str] = [] + markets: Dict[str, Set[str]] = {} + leverage: int = Field( + default=20, gt=0, + client_data=ClientFieldData( + prompt=lambda mi: "Enter the leverage (e.g. 20): ", + prompt_on_new=True)) + + min_funding_rate_profitability: Decimal = Field( + default=0.001, + client_data=ClientFieldData( + prompt=lambda mi: "Enter the min funding rate profitability to enter in a position: ", + prompt_on_new=True + ) + ) + connectors: Set[str] = Field( + default="hyperliquid_perpetual,binance_perpetual", + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter the connectors separated by commas:", + ) + ) + tokens: Set[str] = Field( + default="WIF,FET", + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter the tokens separated by commas:", + ) + ) + position_size_quote: Decimal = Field( + default=100, + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter the position size for each token and exchange (e.g. order amount 100 will open 100 long on hyperliquid and 100 short on binance):", + ) + ) + profitability_to_take_profit: Decimal = Field( + default=0.01, + client_data=ClientFieldData( + prompt_on_new=True, + prompt=lambda mi: "Enter the profitability to take profit (including PNL of positions and fundings received): ", + ) + ) + funding_rate_diff_stop_loss: Decimal = Field( + default=-0.001, + client_data=ClientFieldData( + prompt=lambda mi: "Enter the funding rate difference to stop the position: ", + prompt_on_new=True + ) + ) + trade_profitability_condition_to_enter: bool = Field( + default=False, + client_data=ClientFieldData( + prompt=lambda mi: "Create the position if the trade profitability is positive only: ", + prompt_on_new=True + )) + + @validator("connectors", "tokens", pre=True, allow_reuse=True, always=True) + def validate_sets(cls, v): + if isinstance(v, str): + return set(v.split(",")) + return v + + +class FundingRateArbitrage(StrategyV2Base): + quote_markets_map = { + "hyperliquid_perpetual": "USD", + "binance_perpetual": "USDT" + } + funding_payment_interval_map = { + "binance_perpetual": 60 * 60 * 8, + "hyperliquid_perpetual": 60 * 60 * 1 + } + funding_profitability_interval = 60 * 60 * 24 + + @classmethod + def get_trading_pair_for_connector(cls, token, connector): + return f"{token}-{cls.quote_markets_map.get(connector, 'USDT')}" + + @classmethod + def init_markets(cls, config: FundingRateArbitrageConfig): + markets = {} + for connector in config.connectors: + trading_pairs = {cls.get_trading_pair_for_connector(token, connector) for token in config.tokens} + markets[connector] = trading_pairs + cls.markets = markets + + def __init__(self, connectors: Dict[str, ConnectorBase], config: FundingRateArbitrageConfig): + super().__init__(connectors, config) + self.config = config + self.active_funding_arbitrages = {} + self.stopped_funding_arbitrages = {token: [] for token in self.config.tokens} + + def start(self, clock: Clock, timestamp: float) -> None: + """ + Start the strategy. + :param clock: Clock to use. + :param timestamp: Current time. + """ + self._last_timestamp = timestamp + self.apply_initial_setting() + + def apply_initial_setting(self): + for connector_name, connector in self.connectors.items(): + if self.is_perpetual(connector_name): + position_mode = PositionMode.ONEWAY if connector_name == "hyperliquid_perpetual" else PositionMode.HEDGE + connector.set_position_mode(position_mode) + for trading_pair in self.market_data_provider.get_trading_pairs(connector_name): + connector.set_leverage(trading_pair, self.config.leverage) + + def get_funding_info_by_token(self, token): + """ + This method provides the funding rates across all the connectors + """ + funding_rates = {} + for connector_name, connector in self.connectors.items(): + trading_pair = self.get_trading_pair_for_connector(token, connector_name) + funding_rates[connector_name] = connector.get_funding_info(trading_pair) + return funding_rates + + def get_current_profitability_after_fees(self, token: str, connector_1: str, connector_2: str, side: TradeType): + """ + This methods compares the profitability of buying at market in the two exchanges. If the side is TradeType.BUY + means that the operation is long on connector 1 and short on connector 2. + """ + trading_pair_1 = self.get_trading_pair_for_connector(token, connector_1) + trading_pair_2 = self.get_trading_pair_for_connector(token, connector_2) + + connector_1_price = Decimal(self.market_data_provider.get_price_for_quote_volume( + connector_name=connector_1, + trading_pair=trading_pair_1, + quote_volume=self.config.position_size_quote, + is_buy=side == TradeType.BUY, + ).result_price) + connector_2_price = Decimal(self.market_data_provider.get_price_for_quote_volume( + connector_name=connector_2, + trading_pair=trading_pair_2, + quote_volume=self.config.position_size_quote, + is_buy=side != TradeType.BUY, + ).result_price) + estimated_fees_connector_1 = self.connectors[connector_1].get_fee( + base_currency=trading_pair_1.split("-")[0], + quote_currency=trading_pair_1.split("-")[1], + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.config.position_size_quote / connector_1_price, + price=connector_1_price, + is_maker=False, + position_action=PositionAction.OPEN + ).percent + estimated_fees_connector_2 = self.connectors[connector_2].get_fee( + base_currency=trading_pair_2.split("-")[0], + quote_currency=trading_pair_2.split("-")[1], + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.config.position_size_quote / connector_2_price, + price=connector_2_price, + is_maker=False, + position_action=PositionAction.OPEN + ).percent + + if side == TradeType.BUY: + estimated_trade_pnl_pct = (connector_2_price - connector_1_price) / connector_1_price + else: + estimated_trade_pnl_pct = (connector_1_price - connector_2_price) / connector_2_price + return estimated_trade_pnl_pct - estimated_fees_connector_1 - estimated_fees_connector_2 + + def get_most_profitable_combination(self, funding_info_report: Dict): + best_combination = None + highest_profitability = 0 + for connector_1 in funding_info_report: + for connector_2 in funding_info_report: + if connector_1 != connector_2: + rate_connector_1 = self.get_normalized_funding_rate_in_seconds(funding_info_report, connector_1) + rate_connector_2 = self.get_normalized_funding_rate_in_seconds(funding_info_report, connector_2) + funding_rate_diff = abs(rate_connector_1 - rate_connector_2) * self.funding_profitability_interval + if funding_rate_diff > highest_profitability: + trade_side = TradeType.BUY if rate_connector_1 < rate_connector_2 else TradeType.SELL + highest_profitability = funding_rate_diff + best_combination = (connector_1, connector_2, trade_side, funding_rate_diff) + return best_combination + + def get_normalized_funding_rate_in_seconds(self, funding_info_report, connector_name): + return funding_info_report[connector_name].rate / self.funding_payment_interval_map.get(connector_name, 60 * 60 * 8) + + def create_actions_proposal(self) -> List[CreateExecutorAction]: + """ + In this method we are going to evaluate if a new set of positions has to be created for each of the tokens that + don't have an active arbitrage. + More filters can be applied to limit the creation of the positions, since the current logic is only checking for + positive pnl between funding rate. Is logged and computed the trading profitability at the time for entering + at market to open the possibilities for other people to create variations like sending limit position executors + and if one gets filled buy market the other one to improve the entry prices. + """ + create_actions = [] + for token in self.config.tokens: + if token not in self.active_funding_arbitrages: + funding_info_report = self.get_funding_info_by_token(token) + best_combination = self.get_most_profitable_combination(funding_info_report) + connector_1, connector_2, trade_side, expected_profitability = best_combination + if expected_profitability >= self.config.min_funding_rate_profitability: + current_profitability = self.get_current_profitability_after_fees( + token, connector_1, connector_2, trade_side + ) + if self.config.trade_profitability_condition_to_enter: + if current_profitability < 0: + self.logger().info(f"Best Combination: {connector_1} | {connector_2} | {trade_side}" + f"Funding rate profitability: {expected_profitability}" + f"Trading profitability after fees: {current_profitability}" + f"Trade profitability is negative, skipping...") + continue + self.logger().info(f"Best Combination: {connector_1} | {connector_2} | {trade_side}" + f"Funding rate profitability: {expected_profitability}" + f"Trading profitability after fees: {current_profitability}" + f"Starting executors...") + position_executor_config_1, position_executor_config_2 = self.get_position_executors_config(token, connector_1, connector_2, trade_side) + self.active_funding_arbitrages[token] = { + "connector_1": connector_1, + "connector_2": connector_2, + "executors_ids": [position_executor_config_1.id, position_executor_config_2.id], + "side": trade_side, + "funding_payments": [], + } + return [CreateExecutorAction(executor_config=position_executor_config_1), + CreateExecutorAction(executor_config=position_executor_config_2)] + return create_actions + + def stop_actions_proposal(self) -> List[StopExecutorAction]: + """ + Once the funding rate arbitrage is created we are going to control the funding payments pnl and the current + pnl of each of the executors at the cost of closing the open position at market. + If that PNL is greater than the profitability_to_take_profit + """ + stop_executor_actions = [] + for token, funding_arbitrage_info in self.active_funding_arbitrages.items(): + executors = self.filter_executors( + executors=self.get_all_executors(), + filter_func=lambda x: x.id in funding_arbitrage_info["executors_ids"] + ) + funding_payments_pnl = sum(funding_payment.amount for funding_payment in funding_arbitrage_info["funding_payments"]) + executors_pnl = sum(executor.net_pnl_quote for executor in executors) + take_profit_condition = executors_pnl + funding_payments_pnl > self.config.profitability_to_take_profit * self.config.position_size_quote + funding_info_report = self.get_funding_info_by_token(token) + if funding_arbitrage_info["side"] == TradeType.BUY: + funding_rate_diff = self.get_normalized_funding_rate_in_seconds(funding_info_report, funding_arbitrage_info["connector_2"]) - self.get_normalized_funding_rate_in_seconds(funding_info_report, funding_arbitrage_info["connector_1"]) + else: + funding_rate_diff = self.get_normalized_funding_rate_in_seconds(funding_info_report, funding_arbitrage_info["connector_1"]) - self.get_normalized_funding_rate_in_seconds(funding_info_report, funding_arbitrage_info["connector_2"]) + current_funding_condition = funding_rate_diff * self.funding_profitability_interval < self.config.funding_rate_diff_stop_loss + if take_profit_condition: + self.logger().info("Take profit profitability reached, stopping executors") + self.stopped_funding_arbitrages[token].append(funding_arbitrage_info) + stop_executor_actions.extend([StopExecutorAction(executor_id=executor.id) for executor in executors]) + elif current_funding_condition: + self.logger().info("Funding rate difference reached for stop loss, stopping executors") + self.stopped_funding_arbitrages[token].append(funding_arbitrage_info) + stop_executor_actions.extend([StopExecutorAction(executor_id=executor.id) for executor in executors]) + return stop_executor_actions + + def did_complete_funding_payment(self, funding_payment_completed_event: FundingPaymentCompletedEvent): + """ + Based on the funding payment event received, check if one of the active arbitrages matches to add the event + to the list. + """ + token = funding_payment_completed_event.trading_pair.split("-")[0] + if token in self.active_funding_arbitrages: + self.active_funding_arbitrages[token]["funding_payments"].append(funding_payment_completed_event) + + def get_position_executors_config(self, token, connector_1, connector_2, trade_side): + price = self.market_data_provider.get_price_by_type( + connector_name=connector_1, + trading_pair=self.get_trading_pair_for_connector(token, connector_1), + price_type=PriceType.MidPrice + ) + position_amount = self.config.position_size_quote / price + + position_executor_config_1 = PositionExecutorConfig( + timestamp=self.current_timestamp, + connector_name=connector_1, + trading_pair=self.get_trading_pair_for_connector(token, connector_1), + side=trade_side, + amount=position_amount, + leverage=self.config.leverage, + triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), + ) + position_executor_config_2 = PositionExecutorConfig( + timestamp=self.current_timestamp, + connector_name=connector_2, + trading_pair=self.get_trading_pair_for_connector(token, connector_2), + side=TradeType.BUY if trade_side == TradeType.SELL else TradeType.SELL, + amount=position_amount, + leverage=self.config.leverage, + triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), + ) + return position_executor_config_1, position_executor_config_2 + + def format_status(self) -> str: + original_status = super().format_status() + funding_rate_status = [] + if self.ready_to_trade: + all_funding_info = [] + all_best_paths = [] + for token in self.config.tokens: + token_info = {"token": token} + best_paths_info = {"token": token} + funding_info_report = self.get_funding_info_by_token(token) + best_combination = self.get_most_profitable_combination(funding_info_report) + for connector_name, info in funding_info_report.items(): + token_info[f"{connector_name} Rate (%)"] = self.get_normalized_funding_rate_in_seconds(funding_info_report, connector_name) * self.funding_profitability_interval * 100 + connector_1, connector_2, side, funding_rate_diff = best_combination + profitability_after_fees = self.get_current_profitability_after_fees(token, connector_1, connector_2, side) + best_paths_info["Best Path"] = f"{connector_1}_{connector_2}" + best_paths_info["Best Rate Diff (%)"] = funding_rate_diff * 100 + best_paths_info["Trade Profitability (%)"] = profitability_after_fees * 100 + best_paths_info["Days Trade Prof"] = - profitability_after_fees / funding_rate_diff + best_paths_info["Days to TP"] = (self.config.profitability_to_take_profit - profitability_after_fees) / funding_rate_diff + + time_to_next_funding_info_c1 = funding_info_report[connector_1].next_funding_utc_timestamp - self.current_timestamp + time_to_next_funding_info_c2 = funding_info_report[connector_2].next_funding_utc_timestamp - self.current_timestamp + best_paths_info["Min to Funding 1"] = time_to_next_funding_info_c1 / 60 + best_paths_info["Min to Funding 2"] = time_to_next_funding_info_c2 / 60 + + all_funding_info.append(token_info) + all_best_paths.append(best_paths_info) + funding_rate_status.append(f"\n\n\nMin Funding Rate Profitability: {self.config.min_funding_rate_profitability:.2%}") + funding_rate_status.append(f"Profitability to Take Profit: {self.config.profitability_to_take_profit:.2%}\n") + funding_rate_status.append("Funding Rate Info (Funding Profitability in Days): ") + funding_rate_status.append(format_df_for_printout(df=pd.DataFrame(all_funding_info), table_format="psql",)) + funding_rate_status.append(format_df_for_printout(df=pd.DataFrame(all_best_paths), table_format="psql",)) + for token, funding_arbitrage_info in self.active_funding_arbitrages.items(): + long_connector = funding_arbitrage_info["connector_1"] if funding_arbitrage_info["side"] == TradeType.BUY else funding_arbitrage_info["connector_2"] + short_connector = funding_arbitrage_info["connector_2"] if funding_arbitrage_info["side"] == TradeType.BUY else funding_arbitrage_info["connector_1"] + funding_rate_status.append(f"Token: {token}") + funding_rate_status.append(f"Long connector: {long_connector} | Short connector: {short_connector}") + funding_rate_status.append(f"Funding Payments Collected: {funding_arbitrage_info['funding_payments']}") + funding_rate_status.append(f"Executors: {funding_arbitrage_info['executors_ids']}") + funding_rate_status.append("-" * 50 + "\n") + return original_status + "\n".join(funding_rate_status) diff --git a/scripts/simple_xemm_example.py b/scripts/simple_xemm_example.py index 3c8244b453..f1daac3834 100644 --- a/scripts/simple_xemm_example.py +++ b/scripts/simple_xemm_example.py @@ -119,8 +119,8 @@ def exchanges_df(self) -> pd.DataFrame: Return a custom data frame of prices on maker vs taker exchanges for display purposes """ mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair) - maker_buy_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) - maker_sell_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + maker_buy_result = self.connectors[self.maker_exchange].get_price_for_volume(self.maker_pair, True, self.order_amount) + maker_sell_result = self.connectors[self.maker_exchange].get_price_for_volume(self.maker_pair, False, self.order_amount) taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) maker_buy_spread_bps = (maker_buy_result.result_price - taker_buy_result.result_price) / mid_price * 10000 @@ -139,7 +139,7 @@ def exchanges_df(self) -> pd.DataFrame: data.append([ self.taker_exchange, self.taker_pair, - float(self.connectors[self.taker_exchange].get_mid_price(self.maker_pair)), + float(self.connectors[self.taker_exchange].get_mid_price(self.taker_pair)), float(taker_buy_result.result_price), float(taker_sell_result.result_price), int(-maker_buy_spread_bps), diff --git a/scripts/v2_generic_with_cash_out.py b/scripts/v2_generic_with_cash_out.py index 8277a18604..db6e110666 100644 --- a/scripts/v2_generic_with_cash_out.py +++ b/scripts/v2_generic_with_cash_out.py @@ -35,6 +35,7 @@ def __init__(self, connectors: Dict[str, ConnectorBase], config: GenericV2Strate super().__init__(connectors, config) self.config = config self.cashing_out = False + self.closed_executors_buffer: int = 20 if self.config.time_to_cash_out: self.cash_out_time = self.config.time_to_cash_out + time.time() else: @@ -108,9 +109,10 @@ def stop_actions_proposal(self) -> List[StopExecutorAction]: def apply_initial_setting(self): for controller_id, controller in self.controllers.items(): config_dict = controller.config.dict() - if self.is_perpetual(config_dict.get("connector_name")): - if "position_mode" in config_dict: - self.connectors[config_dict["connector_name"]].set_position_mode(config_dict["position_mode"]) - if "leverage" in config_dict: - self.connectors[config_dict["connector_name"]].set_leverage(leverage=config_dict["leverage"], - trading_pair=config_dict["trading_pair"]) + if config_dict["controller_type"] in ["market_making", "directional_trading"]: + if self.is_perpetual(config_dict.get("connector_name")): + if "position_mode" in config_dict: + self.connectors[config_dict["connector_name"]].set_position_mode(config_dict["position_mode"]) + if "leverage" in config_dict: + self.connectors[config_dict["connector_name"]].set_leverage(leverage=config_dict["leverage"], + trading_pair=config_dict["trading_pair"]) diff --git a/scripts/v2_generic_with_controllers.py b/scripts/v2_generic_with_controllers.py index 070e04f018..7698652461 100644 --- a/scripts/v2_generic_with_controllers.py +++ b/scripts/v2_generic_with_controllers.py @@ -39,9 +39,10 @@ def stop_actions_proposal(self) -> List[StopExecutorAction]: def apply_initial_setting(self): for controller_id, controller in self.controllers.items(): config_dict = controller.config.dict() - if self.is_perpetual(config_dict.get("connector_name")): - if "position_mode" in config_dict: - self.connectors[config_dict["connector_name"]].set_position_mode(config_dict["position_mode"]) - if "leverage" in config_dict: - self.connectors[config_dict["connector_name"]].set_leverage(leverage=config_dict["leverage"], - trading_pair=config_dict["trading_pair"]) + if config_dict["controller_type"] in ["market_making", "directional_trading"]: + if self.is_perpetual(config_dict.get("connector_name")): + if "position_mode" in config_dict: + self.connectors[config_dict["connector_name"]].set_position_mode(config_dict["position_mode"]) + if "leverage" in config_dict: + self.connectors[config_dict["connector_name"]].set_leverage(leverage=config_dict["leverage"], + trading_pair=config_dict["trading_pair"]) diff --git a/scripts/v2_xemm.py b/scripts/v2_xemm.py new file mode 100644 index 0000000000..ad12c7a03a --- /dev/null +++ b/scripts/v2_xemm.py @@ -0,0 +1,128 @@ +import os +from decimal import Decimal +from typing import Dict, List, Set + +from pydantic import Field + +from hummingbot.client.config.config_data_types import ClientFieldData +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import PriceType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.executors.data_types import ConnectorPair +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.smart_components.models.executor_actions import CreateExecutorAction, ExecutorAction +from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase + + +class V2XEMMConfig(StrategyV2ConfigBase): + script_file_name: str = Field(default_factory=lambda: os.path.basename(__file__)) + candles_config: List[CandlesConfig] = [] + controllers_config: List[str] = [] + markets: Dict[str, Set[str]] = {} + maker_connector: str = Field( + default="kucoin", + client_data=ClientFieldData( + prompt=lambda e: "Enter the maker connector: ", + prompt_on_new=True + )) + maker_trading_pair: str = Field( + default="LBR-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the maker trading pair: ", + prompt_on_new=True + )) + taker_connector: str = Field( + default="okx", + client_data=ClientFieldData( + prompt=lambda e: "Enter the taker connector: ", + prompt_on_new=True + )) + taker_trading_pair: str = Field( + default="LBR-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the taker trading pair: ", + prompt_on_new=True + )) + target_profitability: Decimal = Field( + default=0.006, + client_data=ClientFieldData( + prompt=lambda e: "Enter the target profitability: ", + prompt_on_new=True + )) + min_profitability: Decimal = Field( + default=0.003, + client_data=ClientFieldData( + prompt=lambda e: "Enter the minimum profitability: ", + prompt_on_new=True + )) + max_profitability: Decimal = Field( + default=0.008, + client_data=ClientFieldData( + prompt=lambda e: "Enter the maximum profitability: ", + prompt_on_new=True + )) + order_amount_quote: Decimal = Field( + default=100, + client_data=ClientFieldData( + prompt=lambda e: "Enter the order amount in quote asset: ", + prompt_on_new=True + )) + + +class V2XEMM(StrategyV2Base): + @classmethod + def init_markets(cls, config: V2XEMMConfig): + cls.markets = {config.maker_connector: {config.maker_trading_pair}, config.taker_connector: {config.taker_trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase], config: V2XEMMConfig): + super().__init__(connectors, config) + self.config = config + + def determine_executor_actions(self) -> List[ExecutorAction]: + executor_actions = [] + all_executors = self.get_all_executors() + mid_price = self.market_data_provider.get_price_by_type(self.config.maker_connector, self.config.maker_trading_pair, PriceType.MidPrice) + active_buy_executors = self.filter_executors( + executors=all_executors, + filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.BUY + ) + active_sell_executors = self.filter_executors( + executors=all_executors, + filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.SELL + ) + if len(active_buy_executors) == 0: + config = XEMMExecutorConfig( + timestamp=self.current_timestamp, + buying_market=ConnectorPair(connector_name=self.config.maker_connector, + trading_pair=self.config.maker_trading_pair), + selling_market=ConnectorPair(connector_name=self.config.taker_connector, + trading_pair=self.config.taker_trading_pair), + maker_side=TradeType.BUY, + order_amount=self.config.order_amount_quote / mid_price, + min_profitability=self.config.min_profitability, + target_profitability=self.config.target_profitability, + max_profitability=self.config.max_profitability + ) + executor_actions.append(CreateExecutorAction(executor_config=config)) + if len(active_sell_executors) == 0: + config = XEMMExecutorConfig( + timestamp=self.current_timestamp, + buying_market=ConnectorPair(connector_name=self.config.taker_connector, + trading_pair=self.config.taker_trading_pair), + selling_market=ConnectorPair(connector_name=self.config.maker_connector, + trading_pair=self.config.maker_trading_pair), + maker_side=TradeType.SELL, + order_amount=self.config.order_amount_quote / mid_price, + min_profitability=self.config.min_profitability, + target_profitability=self.config.target_profitability, + max_profitability=self.config.max_profitability + ) + executor_actions.append(CreateExecutorAction(executor_config=config)) + return executor_actions + + def format_status(self) -> str: + original_status = super().format_status() + xemm_data = [] + for ex in self.executor_orchestrator.executors["main"]: + xemm_data.append(ex.to_format_status()) + return f"{original_status}\n\n" + '\n'.join(xemm_data) diff --git a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py index c28705256c..29c4c77c8b 100644 --- a/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py +++ b/test/hummingbot/connector/derivative/hyperliquid_perpetual/test_hyperliquid_perpetual_api_order_book_data_source.py @@ -21,7 +21,7 @@ ) from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant from hummingbot.connector.trading_rule import TradingRule -from hummingbot.core.data_type.funding_info import FundingInfo +from hummingbot.core.data_type.funding_info import FundingInfo, FundingInfoUpdate from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType @@ -48,7 +48,7 @@ def setUp(self) -> None: self.connector = HyperliquidPerpetualDerivative( client_config_map, hyperliquid_perpetual_api_key="testkey", - hyperliquid_perpetual_api_secret="13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930", # noqa: mock + hyperliquid_perpetual_api_secret="13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930",# noqa: mock use_vault=False, trading_pairs=[self.trading_pair], ) @@ -85,6 +85,10 @@ def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception + def resume_test_callback(self, *_, **__): + self.resume_test_event.set() + return None + def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret @@ -508,3 +512,66 @@ def _simulate_trading_rules_initialized(self): min_base_amount_increment=Decimal(str(0.000001)), ) } + + @aioresponses() + def test_listen_for_funding_info_cancelled_error_raised(self, mock_api): + endpoint = CONSTANTS.EXCHANGE_INFO_URL + url = web_utils.public_rest_url(endpoint) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?") + ".*") + resp = self.get_funding_info_rest_msg() + mock_api.post(regex_url, body=json.dumps(resp), exception=asyncio.CancelledError) + + mock_queue: asyncio.Queue = asyncio.Queue() + with self.assertRaises(asyncio.CancelledError): + self.async_run_with_timeout(self.data_source.listen_for_funding_info(mock_queue), + timeout=CONSTANTS.FUNDING_RATE_UPDATE_INTERNAL_SECOND + 10) + + self.assertEqual(0, mock_queue.qsize()) + + @aioresponses() + def test_listen_for_funding_info_logs_exception(self, mock_api): + endpoint = CONSTANTS.EXCHANGE_INFO_URL + url = web_utils.public_rest_url(endpoint) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?") + ".*") + resp = self.get_funding_info_rest_msg() + resp[0]["universe"] = "" + mock_api.post(regex_url, body=json.dumps(resp), callback=self.resume_test_callback) + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_funding_info(msg_queue)) + + self.async_run_with_timeout(self.resume_test_event.wait(), + timeout=CONSTANTS.FUNDING_RATE_UPDATE_INTERNAL_SECOND + 10) + + self.assertTrue( + self._is_logged("ERROR", "Unexpected error when processing public funding info updates from exchange")) + + @patch( + "hummingbot.connector.derivative.hyperliquid_perpetual.hyperliquid_perpetual_api_order_book_data_source." + "HyperliquidPerpetualAPIOrderBookDataSource._next_funding_time") + @aioresponses() + def test_listen_for_funding_info_successful(self, next_funding_time_mock, mock_api): + next_funding_time_mock.return_value = 1713272400 + endpoint = CONSTANTS.EXCHANGE_INFO_URL + url = web_utils.public_rest_url(endpoint) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?") + ".*") + resp = self.get_funding_info_rest_msg() + mock_api.post(regex_url, body=json.dumps(resp)) + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_funding_info(msg_queue)) + + msg: FundingInfoUpdate = self.async_run_with_timeout(msg_queue.get(), + timeout=CONSTANTS.FUNDING_RATE_UPDATE_INTERNAL_SECOND + 10) + + self.assertEqual(self.trading_pair, msg.trading_pair) + expected_index_price = Decimal('36717.0') + self.assertEqual(expected_index_price, msg.index_price) + expected_mark_price = Decimal('36733.0') + self.assertEqual(expected_mark_price, msg.mark_price) + expected_funding_time = next_funding_time_mock.return_value + self.assertEqual(expected_funding_time, msg.next_funding_utc_timestamp) + expected_rate = Decimal('0.00001793') + self.assertEqual(expected_rate, msg.rate) diff --git a/test/hummingbot/connector/exchange/binance/test_binance_exchange.py b/test/hummingbot/connector/exchange/binance/test_binance_exchange.py index 37fd9b979b..3d713bcf68 100644 --- a/test/hummingbot/connector/exchange/binance/test_binance_exchange.py +++ b/test/hummingbot/connector/exchange/binance/test_binance_exchange.py @@ -84,10 +84,10 @@ def all_symbols_request_mock_response(self): "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "SPOT", "MARGIN" - ] + ]] }, ] } @@ -149,9 +149,9 @@ def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "MARGIN" - ] + ]] }, { "symbol": self.exchange_symbol_for_tokens("INVALID", "PAIR"), @@ -176,9 +176,9 @@ def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "MARGIN" - ] + ]] }, ] } @@ -226,10 +226,10 @@ def trading_rules_request_mock_response(self): "minNotional": "0.00100000" } ], - "permissions": [ + "permissionSets": [[ "SPOT", "MARGIN" - ] + ]] } ] } @@ -255,10 +255,10 @@ def trading_rules_request_erroneous_mock_response(self): "ocoAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, - "permissions": [ + "permissionSets": [[ "SPOT", "MARGIN" - ] + ]] } ] } @@ -297,9 +297,9 @@ def balance_request_mock_response_for_base_and_quote(self): "locked": "0.00000000" } ], - "permissions": [ + "permissionSets": [[ "SPOT" - ] + ]] } @property @@ -315,7 +315,7 @@ def balance_request_mock_response_only_base(self): "updateTime": 123456789, "accountType": "SPOT", "balances": [{"asset": self.base_asset, "free": "10.0", "locked": "5.0"}], - "permissions": ["SPOT"], + "permissionSets": [["SPOT"]], } @property @@ -1180,9 +1180,9 @@ def test_format_trading_rules__min_notional_present(self): "minNotional": "0.00100000" } ], - "permissions": [ + "permissionSets": [[ "SPOT" - ] + ]] }] exchange_info = {"symbols": trading_rules} @@ -1217,9 +1217,9 @@ def test_format_trading_rules__notional_but_no_min_notional_present(self): "avgPriceMins": 5 } ], - "permissions": [ + "permissionSets": [[ "SPOT" - ] + ]] }] exchange_info = {"symbols": trading_rules} diff --git a/test/hummingbot/connector/exchange/binance/test_binance_utils.py b/test/hummingbot/connector/exchange/binance/test_binance_utils.py index 40c9666bd5..4ab8c14fa5 100644 --- a/test/hummingbot/connector/exchange/binance/test_binance_utils.py +++ b/test/hummingbot/connector/exchange/binance/test_binance_utils.py @@ -17,28 +17,28 @@ def setUpClass(cls) -> None: def test_is_exchange_information_valid(self): invalid_info_1 = { "status": "BREAK", - "permissions": ["MARGIN"], + "permissionSets": [["MARGIN"]], } self.assertFalse(utils.is_exchange_information_valid(invalid_info_1)) invalid_info_2 = { "status": "BREAK", - "permissions": ["SPOT"], + "permissionSets": [["SPOT"]], } self.assertFalse(utils.is_exchange_information_valid(invalid_info_2)) invalid_info_3 = { "status": "TRADING", - "permissions": ["MARGIN"], + "permissionSets": [["MARGIN"]], } self.assertFalse(utils.is_exchange_information_valid(invalid_info_3)) invalid_info_4 = { "status": "TRADING", - "permissions": ["SPOT"], + "permissionSets": [["SPOT"]], } self.assertTrue(utils.is_exchange_information_valid(invalid_info_4)) diff --git a/test/hummingbot/connector/exchange/okx/test_okx_exchange.py b/test/hummingbot/connector/exchange/okx/test_okx_exchange.py index 772a753e61..1b4cc4cc07 100644 --- a/test/hummingbot/connector/exchange/okx/test_okx_exchange.py +++ b/test/hummingbot/connector/exchange/okx/test_okx_exchange.py @@ -404,7 +404,7 @@ def expected_latest_price(self): @property def expected_supported_order_types(self): - return [OrderType.LIMIT, OrderType.LIMIT_MAKER] + return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] @property def expected_trading_rule(self): diff --git a/test/hummingbot/connector/exchange/vertex/test_vertex_web_utils.py b/test/hummingbot/connector/exchange/vertex/test_vertex_web_utils.py index 455e84847b..9cbe2e42a4 100644 --- a/test/hummingbot/connector/exchange/vertex/test_vertex_web_utils.py +++ b/test/hummingbot/connector/exchange/vertex/test_vertex_web_utils.py @@ -8,15 +8,15 @@ class WebUtilsTests(TestCase): def test_public_rest_url(self): url = web_utils.public_rest_url(path_url=CONSTANTS.QUERY_PATH_URL, domain=CONSTANTS.DEFAULT_DOMAIN) - self.assertEqual("https://prod.vertexprotocol-backend.com/query", url) + self.assertEqual("https://gateway.prod.vertexprotocol.com/v1/query", url) url = web_utils.public_rest_url(path_url=CONSTANTS.QUERY_PATH_URL, domain=CONSTANTS.TESTNET_DOMAIN) - self.assertEqual("https://test.vertexprotocol-backend.com/query", url) + self.assertEqual("https://gateway.sepolia-test.vertexprotocol.com/v1/query", url) def test_private_rest_url(self): url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_PATH_URL, domain=CONSTANTS.DEFAULT_DOMAIN) - self.assertEqual("https://prod.vertexprotocol-backend.com/query", url) + self.assertEqual("https://gateway.prod.vertexprotocol.com/v1/query", url) url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_PATH_URL, domain=CONSTANTS.TESTNET_DOMAIN) - self.assertEqual("https://test.vertexprotocol-backend.com/query", url) + self.assertEqual("https://gateway.sepolia-test.vertexprotocol.com/v1/query", url) def test_build_api_factory(self): self.assertIsNotNone(web_utils.build_api_factory()) diff --git a/test/hummingbot/core/rate_oracle/sources/test_binance_rate_source.py b/test/hummingbot/core/rate_oracle/sources/test_binance_rate_source.py index 091816d1b2..e864a018bf 100644 --- a/test/hummingbot/core/rate_oracle/sources/test_binance_rate_source.py +++ b/test/hummingbot/core/rate_oracle/sources/test_binance_rate_source.py @@ -18,10 +18,8 @@ def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() cls.target_token = "COINALPHA" cls.global_token = "HBOT" - cls.binance_pair = f"{cls.target_token}{cls.global_token}" + cls.binance_pair = f"{cls.target_token}-{cls.global_token}" cls.trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote=cls.global_token) - cls.binance_us_pair = f"{cls.target_token}USD" - cls.us_trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote="USD") cls.binance_ignored_pair = "SOMEPAIR" cls.ignored_trading_pair = combine_to_hb_trading_pair(base="SOME", quote="PAIR") @@ -30,7 +28,6 @@ def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): return ret def setup_binance_responses(self, mock_api, expected_rate: Decimal): - pairs_us_url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain="us") pairs_url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL) symbols_response = { # truncated "symbols": [ @@ -39,47 +36,22 @@ def setup_binance_responses(self, mock_api, expected_rate: Decimal): "status": "TRADING", "baseAsset": self.target_token, "quoteAsset": self.global_token, - "permissions": [ + "permissionSets": [[ "SPOT", - ], - }, - { - "symbol": self.binance_us_pair, - "status": "TRADING", - "baseAsset": self.target_token, - "quoteAsset": "USD", - "permissions": [ - "SPOT", - ], + ]], }, { "symbol": self.binance_ignored_pair, "status": "PAUSED", "baseAsset": "SOME", "quoteAsset": "PAIR", - "permissions": [ + "permissionSets": [[ "SPOT", - ], + ]], }, ] } - binance_prices_us_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL, domain="us") - binance_prices_us_response = [ - { - "symbol": self.binance_us_pair, - "bidPrice": "20862.0000", - "bidQty": "0.50000000", - "askPrice": "20865.6100", - "askQty": "0.14500000", - }, - { - "symbol": self.binance_ignored_pair, - "bidPrice": "0", - "bidQty": "0", - "askPrice": "0", - "askQty": "0", - } - ] + binance_prices_global_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL) binance_prices_global_response = [ { @@ -90,9 +62,9 @@ def setup_binance_responses(self, mock_api, expected_rate: Decimal): "askQty": "0.14500000", } ] - mock_api.get(pairs_us_url, body=json.dumps(symbols_response)) + # mock_api.get(pairs_us_url, body=json.dumps(symbols_response)) mock_api.get(pairs_url, body=json.dumps(symbols_response)) - mock_api.get(binance_prices_us_url, body=json.dumps(binance_prices_us_response)) + # mock_api.get(binance_prices_us_url, body=json.dumps(binance_prices_us_response)) mock_api.get(binance_prices_global_url, body=json.dumps(binance_prices_global_response)) @aioresponses() @@ -105,5 +77,5 @@ def test_get_binance_prices(self, mock_api): self.assertIn(self.trading_pair, prices) self.assertEqual(expected_rate, prices[self.trading_pair]) - self.assertIn(self.us_trading_pair, prices) + # self.assertIn(self.us_trading_pair, prices) self.assertNotIn(self.ignored_trading_pair, prices) diff --git a/test/hummingbot/core/rate_oracle/sources/test_binance_us_rate_source.py b/test/hummingbot/core/rate_oracle/sources/test_binance_us_rate_source.py new file mode 100644 index 0000000000..cfc1232917 --- /dev/null +++ b/test/hummingbot/core/rate_oracle/sources/test_binance_us_rate_source.py @@ -0,0 +1,83 @@ +import asyncio +import json +import unittest +from decimal import Decimal +from typing import Awaitable + +from aioresponses import aioresponses + +from hummingbot.connector.exchange.binance import binance_constants as CONSTANTS, binance_web_utils as web_utils +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.rate_oracle.sources.binance_us_rate_source import BinanceUSRateSource + + +class BinanceUSRateSourceTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ev_loop = asyncio.get_event_loop() + cls.target_token = "COINALPHA" + cls.binance_us_pair = f"{cls.target_token}USD" + cls.us_trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote="USD") + cls.binance_ignored_pair = "SOMEPAIR" + cls.ignored_trading_pair = combine_to_hb_trading_pair(base="SOME", quote="PAIR") + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + def setup_binance_us_responses(self, mock_api, expected_rate: Decimal): + pairs_us_url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain="us") + symbols_response = { # truncated + "symbols": [ + { + "symbol": self.binance_us_pair, + "status": "TRADING", + "baseAsset": self.target_token, + "quoteAsset": "USD", + "permissionSets": [[ + "SPOT", + ]], + }, + { + "symbol": self.binance_ignored_pair, + "status": "PAUSED", + "baseAsset": "SOME", + "quoteAsset": "PAIR", + "permissionSets": [[ + "SPOT", + ]], + }, + ] + } + binance_prices_us_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL, domain="us") + binance_prices_us_response = [ + { + "symbol": self.binance_us_pair, + "bidPrice": str(expected_rate - Decimal("0.1")), + "bidQty": "0.50000000", + "askPrice": str(expected_rate + Decimal("0.1")), + "askQty": "0.14500000", + }, + { + "symbol": self.binance_ignored_pair, + "bidPrice": "0", + "bidQty": "0", + "askPrice": "0", + "askQty": "0", + } + ] + mock_api.get(pairs_us_url, body=json.dumps(symbols_response)) + mock_api.get(binance_prices_us_url, body=json.dumps(binance_prices_us_response)) + + @aioresponses() + def test_get_binance_prices(self, mock_api): + expected_rate = Decimal("10") + self.setup_binance_us_responses(mock_api=mock_api, expected_rate=expected_rate) + + rate_source = BinanceUSRateSource() + prices = self.async_run_with_timeout(rate_source.get_prices()) + + self.assertIn(self.us_trading_pair, prices) + self.assertEqual(expected_rate, prices[self.us_trading_pair]) + self.assertNotIn(self.ignored_trading_pair, prices) diff --git a/test/hummingbot/core/rate_oracle/sources/test_coinbase_advanced_trade_rate_source.py b/test/hummingbot/core/rate_oracle/sources/test_coinbase_advanced_trade_rate_source.py new file mode 100644 index 0000000000..4424e37aba --- /dev/null +++ b/test/hummingbot/core/rate_oracle/sources/test_coinbase_advanced_trade_rate_source.py @@ -0,0 +1,101 @@ +import asyncio +import json +from decimal import Decimal +from test.isolated_asyncio_wrapper_test_case import IsolatedAsyncioWrapperTestCase +from typing import Awaitable + +from aioresponses import aioresponses + +from hummingbot.connector.exchange.coinbase_advanced_trade import ( + coinbase_advanced_trade_constants as CONSTANTS, + coinbase_advanced_trade_web_utils as web_utils, +) +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.rate_oracle.sources.coinbase_advanced_trade_rate_source import CoinbaseAdvancedTradeRateSource + + +class CoinbaseAdvancedTradeRateSourceTest(IsolatedAsyncioWrapperTestCase): + global_token = None + target_token = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ev_loop = asyncio.get_event_loop() + cls.target_token = "COINALPHA" + cls.global_token = "HBOT" + cls.coinbase_pair = f"{cls.target_token}{cls.global_token}" + cls.trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote=cls.global_token) + cls.coinbase_us_pair = f"{cls.target_token}USD" + cls.us_trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote="USD") + cls.coinbase_ignored_pair = "SOMEPAIR" + cls.ignored_trading_pair = combine_to_hb_trading_pair(base="SOME", quote="PAIR") + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + def setup_coinbase_responses(self, mock_api, expected_rate: Decimal): + time_url = web_utils.private_rest_url(path_url=CONSTANTS.SERVER_TIME_EP) + mock_api.get(time_url, body=json.dumps({ + "data": { + "iso": "2015-06-23T18:02:51Z", + "epoch": 1435082571 + } + })) + + product_url = web_utils.private_rest_url(path_url=CONSTANTS.ALL_PAIRS_EP) + products_response = { + "products": [ + { + "product_id": "COINALPHA-USD", + "quote_currency_id": "USD", + "base_currency_id": "COINALPHA", + "cancel_only": False, + "is_disabled": False, + "trading_disabled": False, + "auction_mode": False, + "product_type": "SPOT", + "base_min_size": "0.010000000000000000", + "base_max_size": "1000000", + "quote_increment": "0.010000000000000000", + "base_increment": "0.010000000000000000", + "quote_min_size": "0.010000000000000000", + "price": "1", + "supports_limit_orders": True, + "supports_market_orders": True + } + ], + "num_products": 1, + } + mock_api.get(product_url, body=json.dumps(products_response)) + + pairs_url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_RATES_QUOTE_EP.format(quote_token='USD')) + symbols_response = { # truncated + "data": + {"currency": "USD", + "rates": + {"AED": "3.6720916666666667", + "AFN": "88.0120479999997356", + "ALL": "101.75", + "AMD": "386.8585", + "ANG": "1.7968655", + "AOA": "509.99999999999745", + "ARS": "228.661430047360453", + "COINALPHA": "0.1", + } + } + + } + mock_api.get(pairs_url, body=json.dumps(symbols_response)) + + @aioresponses() + def test_get_coinbase_prices(self, mock_api): + expected_rate = Decimal("10") + self.setup_coinbase_responses(mock_api=mock_api, expected_rate=expected_rate) + + rate_source = CoinbaseAdvancedTradeRateSource() + prices = self.async_run_with_timeout(rate_source.get_prices()) + + self.assertIn("COINALPHA-USD", prices) + self.assertEqual(expected_rate, prices["COINALPHA-USD"]) diff --git a/test/hummingbot/core/utils/test_trading_pair_fetcher.py b/test/hummingbot/core/utils/test_trading_pair_fetcher.py index 3436cf33e6..16253c8544 100644 --- a/test/hummingbot/core/utils/test_trading_pair_fetcher.py +++ b/test/hummingbot/core/utils/test_trading_pair_fetcher.py @@ -203,10 +203,10 @@ def test_fetch_all(self, mock_api, con_spec_mock, perp_market_mock, all_connecto "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "SPOT", "MARGIN" - ] + ]] }, { "symbol": "LTCBTC", @@ -231,10 +231,10 @@ def test_fetch_all(self, mock_api, con_spec_mock, perp_market_mock, all_connecto "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "SPOT", "MARGIN" - ] + ]] }, { "symbol": "BNBBTC", @@ -259,9 +259,9 @@ def test_fetch_all(self, mock_api, con_spec_mock, perp_market_mock, all_connecto "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], - "permissions": [ + "permissionSets": [[ "MARGIN" - ] + ]] }, ] } diff --git a/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py b/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py index b6a9ebf5c4..a7454edfa7 100644 --- a/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py +++ b/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py @@ -10,8 +10,8 @@ from hummingbot.smart_components.executors.arbitrage_executor.data_types import ( ArbitrageExecutorConfig, ArbitrageExecutorStatus, - ExchangePair, ) +from hummingbot.smart_components.executors.data_types import ConnectorPair from hummingbot.smart_components.models.executors import TrackedOrder from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -21,8 +21,8 @@ def setUp(self): super().setUp() self.strategy = self.create_mock_strategy() self.arbitrage_config = MagicMock(spec=ArbitrageExecutorConfig) - self.arbitrage_config.buying_market = ExchangePair(connector_name='binance', trading_pair='MATIC-USDT') - self.arbitrage_config.selling_market = ExchangePair(connector_name='uniswap_polygon_mainnet', trading_pair='WMATIC-USDT') + self.arbitrage_config.buying_market = ConnectorPair(connector_name='binance', trading_pair='MATIC-USDT') + self.arbitrage_config.selling_market = ConnectorPair(connector_name='uniswap_polygon_mainnet', trading_pair='WMATIC-USDT') self.arbitrage_config.min_profitability = Decimal('0.01') self.arbitrage_config.order_amount = Decimal('1') self.arbitrage_config.max_retries = 3 diff --git a/test/hummingbot/smart_components/executors/dca_executor/test_dca_executor.py b/test/hummingbot/smart_components/executors/dca_executor/test_dca_executor.py index 6a4fac2412..fad7fc10b3 100644 --- a/test/hummingbot/smart_components/executors/dca_executor/test_dca_executor.py +++ b/test/hummingbot/smart_components/executors/dca_executor/test_dca_executor.py @@ -93,6 +93,7 @@ def test_get_custom_info(self, get_price_mock): 'max_retries': 15, 'min_price': Decimal('60'), 'n_levels': 3, + 'order_ids': [], 'side': TradeType.BUY, 'target_position_average_price': Decimal('73.33333333333333333333333333'), 'total_executed_amount_backup': Decimal('0'), diff --git a/test/hummingbot/smart_components/executors/test_executor_orchestrator.py b/test/hummingbot/smart_components/executors/test_executor_orchestrator.py index 8cbf6075a7..3af37b5198 100644 --- a/test/hummingbot/smart_components/executors/test_executor_orchestrator.py +++ b/test/hummingbot/smart_components/executors/test_executor_orchestrator.py @@ -7,7 +7,8 @@ from hummingbot.connector.trading_rule import TradingRule from hummingbot.core.data_type.common import TradeType from hummingbot.smart_components.executors.arbitrage_executor.arbitrage_executor import ArbitrageExecutor -from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageExecutorConfig, ExchangePair +from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageExecutorConfig +from hummingbot.smart_components.executors.data_types import ConnectorPair from hummingbot.smart_components.executors.dca_executor.data_types import DCAExecutorConfig from hummingbot.smart_components.executors.dca_executor.dca_executor import DCAExecutor from hummingbot.smart_components.executors.executor_orchestrator import ExecutorOrchestrator @@ -53,8 +54,8 @@ def test_execute_actions_create_executor(self, arbitrage_start_mock: MagicMock, trading_pair="ETH-USDT", side=TradeType.BUY, entry_price=Decimal(100), amount=Decimal(10)) arbitrage_executor_config = ArbitrageExecutorConfig( timestamp=1234, order_amount=Decimal(10), min_profitability=Decimal(0.01), - buying_market=ExchangePair(connector_name="binance", trading_pair="ETH-USDT"), - selling_market=ExchangePair(connector_name="coinbase", trading_pair="ETH-USDT"), + buying_market=ConnectorPair(connector_name="binance", trading_pair="ETH-USDT"), + selling_market=ConnectorPair(connector_name="coinbase", trading_pair="ETH-USDT"), ) dca_executor_config = DCAExecutorConfig( timestamp=1234, connector_name="binance", trading_pair="ETH-USDT", diff --git a/test/hummingbot/smart_components/executors/xemm_executor/__init__.py b/test/hummingbot/smart_components/executors/xemm_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/executors/xemm_executor/test_xemm_executor.py b/test/hummingbot/smart_components/executors/xemm_executor/test_xemm_executor.py new file mode 100644 index 0000000000..44fd315d49 --- /dev/null +++ b/test/hummingbot/smart_components/executors/xemm_executor/test_xemm_executor.py @@ -0,0 +1,305 @@ +from decimal import Decimal +from test.isolated_asyncio_wrapper_test_case import IsolatedAsyncioWrapperTestCase +from test.logger_mixin_for_test import LoggerMixinForTest +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +from hummingbot.connector.exchange_py_base import ExchangePyBase +from hummingbot.connector.trading_rule import TradingRule +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import BuyOrderCompletedEvent, BuyOrderCreatedEvent, MarketOrderFailureEvent +from hummingbot.smart_components.executors.data_types import ConnectorPair +from hummingbot.smart_components.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.smart_components.executors.xemm_executor.xemm_executor import XEMMExecutor +from hummingbot.smart_components.models.base import SmartComponentStatus +from hummingbot.smart_components.models.executors import CloseType, TrackedOrder +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class TestXEMMExecutor(IsolatedAsyncioWrapperTestCase, LoggerMixinForTest): + def setUp(self): + super().setUp() + self.strategy = self.create_mock_strategy() + self.xemm_base_config = self.base_config_long + self.update_interval = 0.5 + self.executor = XEMMExecutor(self.strategy, self.xemm_base_config, self.update_interval) + self.set_loggers(loggers=[self.executor.logger()]) + + @property + def base_config_long(self) -> XEMMExecutorConfig: + return XEMMExecutorConfig( + timestamp=1234, + buying_market=ConnectorPair(connector_name='binance', trading_pair='ETH-USDT'), + selling_market=ConnectorPair(connector_name='kucoin', trading_pair='ETH-USDT'), + maker_side=TradeType.BUY, + order_amount=Decimal('100'), + min_profitability=Decimal('0.01'), + target_profitability=Decimal('0.015'), + max_profitability=Decimal('0.02'), + ) + + @property + def base_config_short(self) -> XEMMExecutorConfig: + return XEMMExecutorConfig( + timestamp=1234, + buying_market=ConnectorPair(connector_name='binance', trading_pair='ETH-USDT'), + selling_market=ConnectorPair(connector_name='kucoin', trading_pair='ETH-USDT'), + maker_side=TradeType.BUY, + order_amount=Decimal('100'), + min_profitability=Decimal('0.01'), + target_profitability=Decimal('0.015'), + max_profitability=Decimal('0.02'), + ) + + @staticmethod + def create_mock_strategy(): + market = MagicMock() + market_info = MagicMock() + market_info.market = market + + strategy = MagicMock(spec=ScriptStrategyBase) + type(strategy).market_info = PropertyMock(return_value=market_info) + type(strategy).trading_pair = PropertyMock(return_value="ETH-USDT") + strategy.buy.side_effect = ["OID-BUY-1", "OID-BUY-2", "OID-BUY-3"] + strategy.sell.side_effect = ["OID-SELL-1", "OID-SELL-2", "OID-SELL-3"] + strategy.cancel.return_value = None + binance_connector = MagicMock(spec=ExchangePyBase) + binance_connector.supported_order_types = MagicMock(return_value=[OrderType.LIMIT, OrderType.MARKET]) + kucoin_connector = MagicMock(spec=ExchangePyBase) + kucoin_connector.supported_order_types = MagicMock(return_value=[OrderType.LIMIT, OrderType.MARKET]) + strategy.connectors = { + "binance": binance_connector, + "kucoin": kucoin_connector, + } + return strategy + + def test_is_arbitrage_valid(self): + self.assertTrue(self.executor.is_arbitrage_valid('ETH-USDT', 'ETH-USDT')) + self.assertTrue(self.executor.is_arbitrage_valid('ETH-BUSD', 'ETH-USDT')) + self.assertTrue(self.executor.is_arbitrage_valid('ETH-USDT', 'WETH-USDT')) + self.assertFalse(self.executor.is_arbitrage_valid('ETH-USDT', 'BTC-USDT')) + self.assertFalse(self.executor.is_arbitrage_valid('ETH-USDT', 'ETH-BTC')) + + def test_net_pnl_long(self): + self.executor._status = SmartComponentStatus.TERMINATED + self.executor.maker_order = Mock(spec=TrackedOrder) + self.executor.taker_order = Mock(spec=TrackedOrder) + self.executor.maker_order.executed_amount_base = Decimal('1') + self.executor.taker_order.executed_amount_base = Decimal('1') + self.executor.maker_order.average_executed_price = Decimal('100') + self.executor.taker_order.average_executed_price = Decimal('200') + self.executor.maker_order.cum_fees_quote = Decimal('1') + self.executor.taker_order.cum_fees_quote = Decimal('1') + self.assertEqual(self.executor.net_pnl_quote, Decimal('98')) + self.assertEqual(self.executor.net_pnl_pct, Decimal('0.98')) + + def test_net_pnl_short(self): + self.executor._status = SmartComponentStatus.TERMINATED + self.executor.config = self.base_config_short + self.executor.maker_order = Mock(spec=TrackedOrder) + self.executor.taker_order = Mock(spec=TrackedOrder) + self.executor.maker_order.executed_amount_base = Decimal('1') + self.executor.taker_order.executed_amount_base = Decimal('1') + self.executor.maker_order.average_executed_price = Decimal('100') + self.executor.taker_order.average_executed_price = Decimal('200') + self.executor.maker_order.cum_fees_quote = Decimal('1') + self.executor.taker_order.cum_fees_quote = Decimal('1') + self.assertEqual(self.executor.net_pnl_quote, Decimal('98')) + self.assertEqual(self.executor.net_pnl_pct, Decimal('0.98')) + + @patch.object(XEMMExecutor, 'get_trading_rules') + @patch.object(XEMMExecutor, 'adjust_order_candidates') + def test_validate_sufficient_balance(self, mock_adjust_order_candidates, mock_get_trading_rules): + # Mock trading rules + trading_rules = TradingRule(trading_pair="ETH-USDT", min_order_size=Decimal("0.1"), + min_price_increment=Decimal("0.1"), min_base_amount_increment=Decimal("0.1")) + mock_get_trading_rules.return_value = trading_rules + order_candidate = OrderCandidate( + trading_pair="ETH-USDT", + is_maker=True, + order_type=OrderType.LIMIT, + order_side=TradeType.BUY, + amount=Decimal("1"), + price=Decimal("100") + ) + # Test for sufficient balance + mock_adjust_order_candidates.return_value = [order_candidate] + self.executor.validate_sufficient_balance() + self.assertNotEqual(self.executor.close_type, CloseType.INSUFFICIENT_BALANCE) + + # Test for insufficient balance + order_candidate.amount = Decimal("0") + mock_adjust_order_candidates.return_value = [order_candidate] + self.executor.validate_sufficient_balance() + self.assertEqual(self.executor.close_type, CloseType.INSUFFICIENT_BALANCE) + self.assertEqual(self.executor.status, SmartComponentStatus.TERMINATED) + + @patch.object(XEMMExecutor, "get_resulting_price_for_amount") + @patch.object(XEMMExecutor, "get_tx_cost_in_asset") + async def test_control_task_running_order_not_placed(self, tx_cost_mock, resulting_price_mock): + tx_cost_mock.return_value = Decimal('0.01') + resulting_price_mock.return_value = Decimal("100") + self.executor._status = SmartComponentStatus.RUNNING + await self.executor.control_task() + self.assertEqual(self.executor._status, SmartComponentStatus.RUNNING) + self.assertEqual(self.executor.maker_order.order_id, "OID-BUY-1") + self.assertEqual(self.executor._maker_target_price, Decimal("98.48")) + + @patch.object(XEMMExecutor, "get_resulting_price_for_amount") + @patch.object(XEMMExecutor, "get_tx_cost_in_asset") + async def test_control_task_running_order_placed_refresh_condition_min_profitability(self, tx_cost_mock, + resulting_price_mock): + tx_cost_mock.return_value = Decimal('0.01') + resulting_price_mock.return_value = Decimal("100") + self.executor._status = SmartComponentStatus.RUNNING + self.executor.maker_order = Mock(spec=TrackedOrder) + self.executor.maker_order.order_id = "OID-BUY-1" + self.executor.maker_order.order = InFlightOrder( + creation_timestamp=1234, + trading_pair="ETH-USDT", + client_order_id="OID-BUY-1", + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + amount=Decimal("100"), + price=Decimal("99.5"), + initial_state=OrderState.OPEN, + ) + await self.executor.control_task() + self.assertEqual(self.executor._status, SmartComponentStatus.RUNNING) + self.assertEqual(self.executor.maker_order, None) + + @patch.object(XEMMExecutor, "get_resulting_price_for_amount") + @patch.object(XEMMExecutor, "get_tx_cost_in_asset") + async def test_control_task_running_order_placed_refresh_condition_max_profitability(self, tx_cost_mock, + resulting_price_mock): + tx_cost_mock.return_value = Decimal('0.01') + resulting_price_mock.return_value = Decimal("103") + self.executor._status = SmartComponentStatus.RUNNING + self.executor.maker_order = Mock(spec=TrackedOrder) + self.executor.maker_order.order_id = "OID-BUY-1" + self.executor.maker_order.order = InFlightOrder( + creation_timestamp=1234, + trading_pair="ETH-USDT", + client_order_id="OID-BUY-1", + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + amount=Decimal("100"), + price=Decimal("99.5"), + initial_state=OrderState.OPEN, + ) + await self.executor.control_task() + self.assertEqual(self.executor._status, SmartComponentStatus.RUNNING) + self.assertEqual(self.executor.maker_order, None) + + async def test_control_task_shut_down_process(self): + self.executor.maker_order = Mock(spec=TrackedOrder) + self.executor.maker_order.is_done = True + self.executor.taker_order = Mock(spec=TrackedOrder) + self.executor.taker_order.is_done = True + self.executor._status = SmartComponentStatus.SHUTTING_DOWN + await self.executor.control_task() + self.assertEqual(self.executor._status, SmartComponentStatus.TERMINATED) + + @patch.object(XEMMExecutor, "get_in_flight_order") + def test_process_order_created_event(self, in_flight_order_mock): + self.executor._status = SmartComponentStatus.RUNNING + in_flight_order_mock.side_effect = [ + InFlightOrder( + client_order_id="OID-BUY-1", + creation_timestamp=1234, + trading_pair="ETH-USDT", + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + amount=Decimal("100"), + price=Decimal("100"), + ), + InFlightOrder( + client_order_id="OID-SELL-1", + creation_timestamp=1234, + trading_pair="ETH-USDT", + order_type=OrderType.MARKET, + trade_type=TradeType.SELL, + amount=Decimal("100"), + price=Decimal("100"), + ) + ] + + self.executor.maker_order = TrackedOrder(order_id="OID-BUY-1") + self.executor.taker_order = TrackedOrder(order_id="OID-SELL-1") + buy_order_created_event = BuyOrderCreatedEvent( + timestamp=1234, + type=OrderType.LIMIT, + creation_timestamp=1233, + order_id="OID-BUY-1", + trading_pair="ETH-USDT", + amount=Decimal("100"), + price=Decimal("100"), + ) + sell_order_created_event = BuyOrderCreatedEvent( + timestamp=1234, + type=OrderType.MARKET, + creation_timestamp=1233, + order_id="OID-SELL-1", + trading_pair="ETH-USDT", + amount=Decimal("100"), + price=Decimal("100"), + ) + self.assertEqual(self.executor.maker_order.order, None) + self.assertEqual(self.executor.taker_order.order, None) + self.executor.process_order_created_event(1, MagicMock(), buy_order_created_event) + self.assertEqual(self.executor.maker_order.order.client_order_id, "OID-BUY-1") + self.executor.process_order_created_event(1, MagicMock(), sell_order_created_event) + self.assertEqual(self.executor.taker_order.order.client_order_id, "OID-SELL-1") + + def test_process_order_completed_event(self): + self.executor._status = SmartComponentStatus.RUNNING + self.executor.maker_order = TrackedOrder(order_id="OID-BUY-1") + self.assertEqual(self.executor.taker_order, None) + buy_order_created_event = BuyOrderCompletedEvent( + base_asset="ETH", + quote_asset="USDT", + base_asset_amount=Decimal("100"), + quote_asset_amount=Decimal("100"), + order_type=OrderType.LIMIT, + timestamp=1234, + order_id="OID-BUY-1", + ) + self.executor.process_order_completed_event(1, MagicMock(), buy_order_created_event) + self.assertEqual(self.executor.status, SmartComponentStatus.SHUTTING_DOWN) + self.assertEqual(self.executor.taker_order.order_id, "OID-SELL-1") + + def test_process_order_failed_event(self): + self.executor.maker_order = TrackedOrder(order_id="OID-BUY-1") + maker_failure_event = MarketOrderFailureEvent( + timestamp=1234, + order_id="OID-BUY-1", + order_type=OrderType.LIMIT, + ) + self.executor.process_order_failed_event(1, MagicMock(), maker_failure_event) + self.assertEqual(self.executor.maker_order, None) + + self.executor.taker_order = TrackedOrder(order_id="OID-SELL-0") + taker_failure_event = MarketOrderFailureEvent( + timestamp=1234, + order_id="OID-SELL-0", + order_type=OrderType.MARKET, + ) + self.executor.process_order_failed_event(1, MagicMock(), taker_failure_event) + self.assertEqual(self.executor.taker_order.order_id, "OID-SELL-1") + + def test_get_custom_info(self): + self.assertEqual(self.executor.get_custom_info(), {'maker_connector': 'binance', + 'maker_trading_pair': 'ETH-USDT', + 'max_profitability': Decimal('0.02'), + 'min_profitability': Decimal('0.01'), + 'side': TradeType.BUY, + 'taker_connector': 'kucoin', + 'taker_trading_pair': 'ETH-USDT', + 'target_profitability_pct': Decimal('0.015'), + 'trade_profitability': Decimal('0'), + 'tx_cost': Decimal('1'), + 'tx_cost_pct': Decimal('1')}) + + def test_to_format_status(self): + self.assertIn("Maker Side: TradeType.BUY", self.executor.to_format_status()) diff --git a/test/hummingbot/strategy/test_strategy_v2_base.py b/test/hummingbot/strategy/test_strategy_v2_base.py index 1c483a9687..a35f1b1bc1 100644 --- a/test/hummingbot/strategy/test_strategy_v2_base.py +++ b/test/hummingbot/strategy/test_strategy_v2_base.py @@ -10,7 +10,10 @@ from hummingbot.core.clock import Clock from hummingbot.core.clock_mode import ClockMode from hummingbot.core.data_type.common import PositionMode, TradeType -from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorConfig +from hummingbot.smart_components.executors.position_executor.data_types import ( + PositionExecutorConfig, + TripleBarrierConfig, +) from hummingbot.smart_components.models.base import SmartComponentStatus from hummingbot.smart_components.models.executors import CloseType from hummingbot.smart_components.models.executors_info import ExecutorInfo, PerformanceReport @@ -261,8 +264,22 @@ def test_executors_info_to_df(self): self.assertIsInstance(df, pd.DataFrame) self.assertEqual(len(df), 2) self.assertEqual(list(df.columns), - ["id", "timestamp", "type", "status", "net_pnl_pct", "net_pnl_quote", "cum_fees_quote", - "is_trading", "filled_amount_quote", "close_type"]) + ['id', + 'timestamp', + 'type', + 'close_timestamp', + 'close_type', + 'status', + 'config', + 'net_pnl_pct', + 'net_pnl_quote', + 'cum_fees_quote', + 'filled_amount_quote', + 'is_active', + 'is_trading', + 'custom_info', + 'controller_id', + 'side']) self.assertEqual(df.iloc[0]['id'], '2') # Since the dataframe is sorted by status self.assertEqual(df.iloc[1]['id'], '1') self.assertEqual(df.iloc[0]['status'], SmartComponentStatus.RUNNING) @@ -290,32 +307,29 @@ def test_format_status(self, mock_super_format_status): controller_mock.to_format_status.return_value = ["Mock status for controller"] self.strategy.controllers = {"controller_1": controller_mock} - # Mocking generate_performance_report - mock_report = MagicMock() - mock_report.realized_pnl_quote = Decimal("100.00") - mock_report.unrealized_pnl_quote = Decimal("50.00") - mock_report.global_pnl_quote = Decimal("150.00") - mock_report.global_pnl_pct = Decimal("10.00") - mock_report.volume_traded = Decimal("1500.00") - mock_report.close_type_counts = {"close_type_1": 1, "close_type_2": 2} - self.strategy.executor_orchestrator.generate_performance_report = MagicMock(return_value=mock_report) - - # Expected data - expected_controller_performance_info = [ - "Realized PNL (Quote): 100.00 | Unrealized PNL (Quote): 50.00", - "--> Global PNL (Quote): 150.00 | Global PNL (%): 10.00%", - "Total Volume Traded: 1500.00", - "Close Types Count:", - " close_type_1: 1", - " close_type_2: 2" - ] - - expected_global_performance_summary = [ - "Global PNL (Quote): 150.00 | Global PNL (%): 10.00% | Total Volume Traded (Global): 1500.00", - "Global Close Types Count:", - " close_type_1: 1", - " close_type_2: 2" - ] + mock_report_controller_1 = MagicMock() + mock_report_controller_1.realized_pnl_quote = Decimal("100.00") + mock_report_controller_1.unrealized_pnl_quote = Decimal("50.00") + mock_report_controller_1.global_pnl_quote = Decimal("150.00") + mock_report_controller_1.global_pnl_pct = Decimal("15.00") + mock_report_controller_1.volume_traded = Decimal("1000.00") + mock_report_controller_1.close_type_counts = {CloseType.TAKE_PROFIT: 10, CloseType.STOP_LOSS: 5} + + # Mocking generate_performance_report for main controller + mock_report_main = MagicMock() + mock_report_main.realized_pnl_quote = Decimal("200.00") + mock_report_main.unrealized_pnl_quote = Decimal("75.00") + mock_report_main.global_pnl_quote = Decimal("275.00") + mock_report_main.global_pnl_pct = Decimal("15.00") + mock_report_main.volume_traded = Decimal("2000.00") + mock_report_main.close_type_counts = {CloseType.TAKE_PROFIT: 2, CloseType.STOP_LOSS: 3} + self.strategy.executor_orchestrator.generate_performance_report = MagicMock(side_effect=[mock_report_controller_1, mock_report_main]) + # Mocking get_executors_by_controller for main controller to return an empty list + self.strategy.get_executors_by_controller = MagicMock(return_value=[ExecutorInfo( + id="12312", timestamp=1234567890, status=SmartComponentStatus.TERMINATED, + config=self.get_position_config_market_short(), net_pnl_pct=Decimal(0), net_pnl_quote=Decimal(0), + cum_fees_quote=Decimal(0), filled_amount_quote=Decimal(0), is_active=False, is_trading=False, + custom_info={}, type="position_executor", controller_id="main")]) # Call format_status status = self.strategy.format_status() @@ -323,7 +337,13 @@ def test_format_status(self, mock_super_format_status): # Assertions self.assertIn(original_status, status) self.assertIn("Mock status for controller", status) - for line in expected_controller_performance_info: - self.assertIn(line, status) - for line in expected_global_performance_summary: - self.assertIn(line, status) + self.assertIn("Controller: controller_1", status) + self.assertIn("Realized PNL (Quote): 100.00", status) + self.assertIn("Unrealized PNL (Quote): 50.00", status) + self.assertIn("Global PNL (Quote): 150", status) + + def get_position_config_market_short(self): + return PositionExecutorConfig(id="test-2", timestamp=1234567890, trading_pair="ETH-USDT", + connector_name="binance", + side=TradeType.SELL, entry_price=Decimal("100"), amount=Decimal("1"), + triple_barrier_config=TripleBarrierConfig())