From d8933de8586d71b2d064b60c81d1c77fe09d914c Mon Sep 17 00:00:00 2001 From: cardosofede Date: Mon, 24 Jun 2024 22:30:37 +0200 Subject: [PATCH] (feat) add generic controllers --- bots/controllers/__init__.py | 0 bots/controllers/generic/__init__.py | 0 .../generic/spot_perp_arbitrage.py | 196 ++++++++++++++++++ .../generic/xemm_multiple_levels.py | 162 +++++++++++++++ 4 files changed, 358 insertions(+) create mode 100644 bots/controllers/__init__.py create mode 100644 bots/controllers/generic/__init__.py create mode 100644 bots/controllers/generic/spot_perp_arbitrage.py create mode 100644 bots/controllers/generic/xemm_multiple_levels.py diff --git a/bots/controllers/__init__.py b/bots/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/controllers/generic/__init__.py b/bots/controllers/generic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/controllers/generic/spot_perp_arbitrage.py b/bots/controllers/generic/spot_perp_arbitrage.py new file mode 100644 index 0000000..f2f434b --- /dev/null +++ b/bots/controllers/generic/spot_perp_arbitrage.py @@ -0,0 +1,196 @@ +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, PositionAction, OrderType +from hummingbot.data_feed.candles_feed.data_types import CandlesConfig +from hummingbot.strategy_v2.controllers.controller_base import ControllerBase, ControllerConfigBase +from hummingbot.strategy_v2.executors.data_types import ConnectorPair +from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, \ + TripleBarrierConfig +from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction + + +class SpotPerpArbitrageConfig(ControllerConfigBase): + controller_name: str = "spot_perp_arbitrage" + candles_config: List[CandlesConfig] = [] + spot_connector: str = Field( + default="binance", + client_data=ClientFieldData( + prompt=lambda e: "Enter the spot connector: ", + prompt_on_new=True + )) + spot_trading_pair: str = Field( + default="DOGE-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the spot trading pair: ", + prompt_on_new=True + )) + perp_connector: str = Field( + default="binance_perpetual", + client_data=ClientFieldData( + prompt=lambda e: "Enter the perp connector: ", + prompt_on_new=True + )) + perp_trading_pair: str = Field( + default="DOGE-USDT", + client_data=ClientFieldData( + prompt=lambda e: "Enter the perp trading pair: ", + prompt_on_new=True + )) + profitability: Decimal = Field( + default=0.002, + client_data=ClientFieldData( + prompt=lambda e: "Enter the minimum profitability: ", + prompt_on_new=True + )) + position_size_quote: float = Field( + default=50, + client_data=ClientFieldData( + prompt=lambda e: "Enter the position size in quote currency: ", + prompt_on_new=True + )) + + def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]: + if self.spot_connector not in markets: + markets[self.spot_connector] = set() + markets[self.spot_connector].add(self.spot_trading_pair) + if self.perp_connector not in markets: + markets[self.perp_connector] = set() + markets[self.perp_connector].add(self.perp_trading_pair) + return markets + + +class SpotPerpArbitrage(ControllerBase): + + def __init__(self, config: SpotPerpArbitrageConfig, *args, **kwargs): + self.config = config + super().__init__(config, *args, **kwargs) + + @property + def spot_connector(self): + return self.market_data_provider.connectors[self.config.spot_connector] + + @property + def perp_connector(self): + return self.market_data_provider.connectors[self.config.perp_connector] + + def get_current_profitability_after_fees(self): + """ + 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. + """ + spot_trading_pair = self.config.spot_trading_pair + perp_trading_pair = self.config.perp_trading_pair + + connector_spot_price = Decimal(self.market_data_provider.get_price_for_quote_volume( + connector_name=self.config.spot_connector, + trading_pair=spot_trading_pair, + quote_volume=self.config.position_size_quote, + is_buy=True, + ).result_price) + connector_perp_price = Decimal(self.market_data_provider.get_price_for_quote_volume( + connector_name=self.config.spot_connector, + trading_pair=perp_trading_pair, + quote_volume=self.config.position_size_quote, + is_buy=False, + ).result_price) + estimated_fees_spot_connector = self.spot_connector.get_fee( + base_currency=spot_trading_pair.split("-")[0], + quote_currency=spot_trading_pair.split("-")[1], + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.config.position_size_quote / float(connector_spot_price), + price=connector_spot_price, + is_maker=False, + ).percent + estimated_fees_perp_connector = self.perp_connector.get_fee( + base_currency=perp_trading_pair.split("-")[0], + quote_currency=perp_trading_pair.split("-")[1], + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.config.position_size_quote / float(connector_perp_price), + price=connector_perp_price, + is_maker=False, + position_action=PositionAction.OPEN + ).percent + + estimated_trade_pnl_pct = (connector_perp_price - connector_spot_price) / connector_spot_price + return estimated_trade_pnl_pct - estimated_fees_spot_connector - estimated_fees_perp_connector + + def is_active_arbitrage(self): + executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: e.is_active + ) + return len(executors) > 0 + + def current_pnl_pct(self): + executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: e.is_active + ) + filled_amount = sum(e.filled_amount_quote for e in executors) + return sum(e.net_pnl_quote for e in executors) / filled_amount if filled_amount > 0 else 0 + + async def update_processed_data(self): + self.processed_data = { + "profitability": self.get_current_profitability_after_fees(), + "active_arbitrage": self.is_active_arbitrage(), + "current_pnl": self.current_pnl_pct() + } + + def determine_executor_actions(self) -> List[ExecutorAction]: + executor_actions = [] + executor_actions.extend(self.create_new_arbitrage_actions()) + executor_actions.extend(self.stop_arbitrage_actions()) + return executor_actions + + def create_new_arbitrage_actions(self): + create_actions = [] + if not self.processed_data["active_arbitrage"] and self.processed_data["profitability"] > self.config.profitability: + mid_price = self.market_data_provider.get_price_by_type(self.config.spot_connector, self.config.spot_trading_pair, PriceType.MidPrice) + create_actions.append(CreateExecutorAction( + controller_id=self.config.id, + executor_config=PositionExecutorConfig( + timestamp=self.market_data_provider.time(), + connector_name=self.config.spot_connector, + trading_pair=self.config.spot_trading_pair, + side=TradeType.BUY, + amount=Decimal(self.config.position_size_quote) / mid_price, + triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), + ) + )) + create_actions.append(CreateExecutorAction( + controller_id=self.config.id, + executor_config=PositionExecutorConfig( + timestamp=self.market_data_provider.time(), + connector_name=self.config.perp_connector, + trading_pair=self.config.perp_trading_pair, + side=TradeType.SELL, + amount=Decimal(self.config.position_size_quote) / mid_price, + triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), + )) + ) + return create_actions + + def stop_arbitrage_actions(self): + stop_actions = [] + if self.processed_data["current_pnl"] > 0.003: + executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: e.is_active + ) + for executor in executors: + stop_actions.append(StopExecutorAction(controller_id=self.config.id, executor_id=executor.id)) + + def to_format_status(self) -> List[str]: + return [f"Current profitability: {self.processed_data['profitability']} | Min profitability: {self.config.profitability}", + f"Active arbitrage: {self.processed_data['active_arbitrage']}", + f"Current PnL: {self.processed_data['current_pnl']}"] diff --git a/bots/controllers/generic/xemm_multiple_levels.py b/bots/controllers/generic/xemm_multiple_levels.py new file mode 100644 index 0000000..d12bb52 --- /dev/null +++ b/bots/controllers/generic/xemm_multiple_levels.py @@ -0,0 +1,162 @@ +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.data_types import CandlesConfig +from hummingbot.strategy_v2.controllers.controller_base import ControllerBase, ControllerConfigBase +from hummingbot.strategy_v2.executors.data_types import ConnectorPair +from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig +from hummingbot.strategy_v2.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 + )) + max_executors_imbalance: int = Field( + default=1, + client_data=ClientFieldData( + prompt=lambda e: "Enter the maximum executors imbalance: ", + 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 + ) + stopped_buy_executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: e.is_done and e.config.maker_side == TradeType.BUY and e.filled_amount_quote != 0 + ) + stopped_sell_executors = self.filter_executors( + executors=self.executors_info, + filter_func=lambda e: e.is_done and e.config.maker_side == TradeType.SELL and e.filled_amount_quote != 0 + ) + imbalance = len(stopped_buy_executors) - len(stopped_sell_executors) + 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 and imbalance < self.config.max_executors_imbalance: + config = XEMMExecutorConfig( + controller_id=self.config.id, + timestamp=self.market_data_provider.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 and imbalance > -self.config.max_executors_imbalance: + 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", )]