From fff6c189ebd601f734cfe98cf752f847345a2ff5 Mon Sep 17 00:00:00 2001 From: haitranwang Date: Thu, 18 Jul 2024 15:15:27 +0700 Subject: [PATCH 1/4] update for evm --- hummingbot/client/settings.py | 6 +++++- hummingbot/connector/gateway/amm/gateway_aura_amm.py | 12 ++++++++---- hummingbot/connector/gateway/common_types.py | 1 + hummingbot/core/data_type/trade_fee.py | 1 + hummingbot/core/gateway/__init__.py | 2 ++ hummingbot/core/utils/gateway_config_utils.py | 3 ++- .../arbitrage_executor/arbitrage_executor.py | 1 + 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/hummingbot/client/settings.py b/hummingbot/client/settings.py index d5aa373f1d1..b6a4effa423 100644 --- a/hummingbot/client/settings.py +++ b/hummingbot/client/settings.py @@ -122,7 +122,11 @@ def get_connector_spec_from_market_name(market_name: str) -> Optional[Dict[str, connector = 'halotrade' network = chain chain = 'aura' - else: + elif chain in ['auraevm']: + connector = 'halotradeevm' + network = 'mainnet' + chain = 'auraevm' + else: connector, network = market_name.split(f"_{chain}_") return GatewayConnectionSetting.get_connector_spec(connector, chain, network) return None diff --git a/hummingbot/connector/gateway/amm/gateway_aura_amm.py b/hummingbot/connector/gateway/amm/gateway_aura_amm.py index 8c56efb978f..72ce8c56b1c 100644 --- a/hummingbot/connector/gateway/amm/gateway_aura_amm.py +++ b/hummingbot/connector/gateway/amm/gateway_aura_amm.py @@ -1,6 +1,6 @@ import asyncio from decimal import Decimal -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from hummingbot.connector.gateway.amm.gateway_evm_amm import GatewayEVMAMM from hummingbot.connector.gateway.gateway_in_flight_order import GatewayInFlightOrder @@ -8,8 +8,8 @@ from hummingbot.core.data_type.in_flight_order import OrderState, OrderUpdate from hummingbot.core.data_type.trade_fee import TokenAmount from hummingbot.core.event.events import TradeType -from hummingbot.core.gateway import check_transaction_exceptions from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather + if TYPE_CHECKING: from hummingbot.client.config.config_helpers import ClientConfigAdapter @@ -66,6 +66,7 @@ async def stop_network(self): if self._get_gas_estimate_task is not None: self._get_gas_estimate_task.cancel() self._get_chain_info_task = None + @property def ready(self): return all(self.status_dict.values()) @@ -149,6 +150,7 @@ def parse_price_response( # return None return Decimal(str(price)) return None + async def _status_polling_loop(self): await self.update_balances(on_interval=False) while True: @@ -168,6 +170,8 @@ async def _status_polling_loop(self): self.logger().error(str(e), exc_info=True) async def update_order_status(self, tracked_orders: List[GatewayInFlightOrder]): + self.logger().error("SELFFFFFFF: ", self) + self.logger().error("tracked_orders: ", tracked_orders) """ Calls REST API to get status update for each in-flight amm orders. """ @@ -213,7 +217,7 @@ async def update_order_status(self, tracked_orders: List[GatewayInFlightOrder]): new_state=OrderState.FILLED, ) self._order_tracker.process_order_update(order_update) - else: + else: order_update: OrderUpdate = OrderUpdate( client_order_id=tracked_order.client_order_id, trading_pair=tracked_order.trading_pair, @@ -247,4 +251,4 @@ async def get_allowances(self): pass async def all_trading_pairs(self): - pass \ No newline at end of file + pass diff --git a/hummingbot/connector/gateway/common_types.py b/hummingbot/connector/gateway/common_types.py index 14302cf8677..316b2079c6d 100644 --- a/hummingbot/connector/gateway/common_types.py +++ b/hummingbot/connector/gateway/common_types.py @@ -7,6 +7,7 @@ class Chain(Enum): ETHEREUM = ('ethereum', 'ETH') TEZOS = ('tezos', 'XTZ') AURA = ('aura', 'AURA') + AURAEVM = ('auraevm', 'AURA') def __init__(self, chain: str, native_currency: str): self.chain = chain diff --git a/hummingbot/core/data_type/trade_fee.py b/hummingbot/core/data_type/trade_fee.py index c1fcc0103c1..5ae054e1984 100644 --- a/hummingbot/core/data_type/trade_fee.py +++ b/hummingbot/core/data_type/trade_fee.py @@ -230,6 +230,7 @@ def fee_amount_in_token( def _are_tokens_interchangeable(self, first_token: str, second_token: str): interchangeable_tokens = [ {"WETH", "ETH"}, + {"WAURA", "AURA"}, {"WBNB", "BNB"}, {"WMATIC", "MATIC"}, {"WAVAX", "AVAX"}, diff --git a/hummingbot/core/gateway/__init__.py b/hummingbot/core/gateway/__init__.py index bb64b314279..cc90f919262 100644 --- a/hummingbot/core/gateway/__init__.py +++ b/hummingbot/core/gateway/__init__.py @@ -119,6 +119,8 @@ def check_transaction_exceptions( gas_limit_threshold: int = 0 elif chain == Chain.AURA.chain: gas_limit_threshold: int = 0 + elif chain == Chain.AURAEVM.chain: + gas_limit_threshold: int = 0 else: raise ValueError(f"Unsupported chain: {chain}") if gas_limit < gas_limit_threshold: diff --git a/hummingbot/core/utils/gateway_config_utils.py b/hummingbot/core/utils/gateway_config_utils.py index 0510e050b5b..9488493018d 100644 --- a/hummingbot/core/utils/gateway_config_utils.py +++ b/hummingbot/core/utils/gateway_config_utils.py @@ -18,7 +18,8 @@ "tezos": "XTZ", "kujira": "KUJI", "euphoria": "EAURA", - "xstaxy": "AURA" + "xstaxy": "AURA", + "auraevm": "AURA", } SUPPORTED_CHAINS = set(native_tokens.keys()) diff --git a/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py index 1da2a2266d4..6242aa8bfa5 100644 --- a/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py +++ b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py @@ -40,6 +40,7 @@ def is_closed(self): def _are_tokens_interchangeable(first_token: str, second_token: str): interchangeable_tokens = [ {"WETH", "ETH"}, + {"WAURA", "AURA"}, {"WBTC", "BTC"}, {"WBNB", "BNB"}, {"WMATIC", "MATIC"}, From f68ea24d55866449d8583d57b30c6e742024db6c Mon Sep 17 00:00:00 2001 From: haitranwang Date: Mon, 29 Jul 2024 11:55:26 +0700 Subject: [PATCH 2/4] add boost volume strategy --- .../connector/gateway/amm/gateway_aura_amm.py | 2 - hummingbot/strategy/boost_volume/__init__.py | 0 .../strategy/boost_volume/boost_volume.py | 282 ++++++++++++++++++ .../boost_volume/boost_volume_config_map.py | 109 +++++++ hummingbot/strategy/boost_volume/start.py | 51 ++++ .../conf_boost_volume_strategy_TEMPLATE.yml | 36 +++ 6 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 hummingbot/strategy/boost_volume/__init__.py create mode 100644 hummingbot/strategy/boost_volume/boost_volume.py create mode 100644 hummingbot/strategy/boost_volume/boost_volume_config_map.py create mode 100644 hummingbot/strategy/boost_volume/start.py create mode 100644 hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml diff --git a/hummingbot/connector/gateway/amm/gateway_aura_amm.py b/hummingbot/connector/gateway/amm/gateway_aura_amm.py index 72ce8c56b1c..9f15a84285e 100644 --- a/hummingbot/connector/gateway/amm/gateway_aura_amm.py +++ b/hummingbot/connector/gateway/amm/gateway_aura_amm.py @@ -170,8 +170,6 @@ async def _status_polling_loop(self): self.logger().error(str(e), exc_info=True) async def update_order_status(self, tracked_orders: List[GatewayInFlightOrder]): - self.logger().error("SELFFFFFFF: ", self) - self.logger().error("tracked_orders: ", tracked_orders) """ Calls REST API to get status update for each in-flight amm orders. """ diff --git a/hummingbot/strategy/boost_volume/__init__.py b/hummingbot/strategy/boost_volume/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/hummingbot/strategy/boost_volume/boost_volume.py b/hummingbot/strategy/boost_volume/boost_volume.py new file mode 100644 index 00000000000..cb96a5625b9 --- /dev/null +++ b/hummingbot/strategy/boost_volume/boost_volume.py @@ -0,0 +1,282 @@ +import asyncio +import logging +from decimal import Decimal +from functools import lru_cache +from typing import Callable, Dict, List, Optional, Tuple, cast + +from hummingbot.client.settings import AllConnectorSettings, GatewayConnectionSetting +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.connector.gateway.amm.gateway_evm_amm import GatewayEVMAMM +from hummingbot.connector.gateway.gateway_price_shim import GatewayPriceShim +from hummingbot.core.clock import Clock +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.data_type.market_order import MarketOrder +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + MarketOrderFailureEvent, + OrderCancelledEvent, + OrderExpiredEvent, + OrderType, + SellOrderCompletedEvent, +) +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.logger import HummingbotLogger +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.strategy_py_base import StrategyPyBase + +NaN = float("nan") +s_decimal_zero = Decimal(0) +amm_logger = None + + +class BoostVolumeStrategy(StrategyPyBase): + """ + This is a basic boosting strategy which can be used for most types of connectors (CEX, DEX or AMM). + """ + + _market_info_1: MarketTradingPairTuple + _order_amount: Decimal + _market_1_slippage_buffer: Decimal + _number_of_orders: int + _last_no_boost_reported: float + _all_markets_ready: bool + _ev_loop: asyncio.AbstractEventLoop + _main_task: Optional[asyncio.Task] + _last_timestamp: float + _status_report_interval: float + _quote_eth_rate_fetch_loop_task: Optional[asyncio.Task] + _market_1_quote_eth_rate: None # XXX (martin_kou): Why are these here? + _rate_source: Optional[RateOracle] + _cancel_outdated_orders_task: Optional[asyncio.Task] + _gateway_transaction_cancel_interval: int + + @classmethod + def logger(cls) -> HummingbotLogger: + global amm_logger + if amm_logger is None: + amm_logger = logging.getLogger(__name__) + return amm_logger + + def init_params(self, + market_info_1: MarketTradingPairTuple, + order_amount: Decimal, + market_1_slippage_buffer: Decimal = Decimal("0"), + number_of_orders: int = 10, + status_report_interval: float = 900, + gateway_transaction_cancel_interval: int = 600, + rate_source: Optional[RateOracle] = RateOracle.get_instance(), + ): + + self._market_info_1 = market_info_1 + self._order_amount = order_amount + self._market_1_slippage_buffer = market_1_slippage_buffer + self._number_of_orders = number_of_orders + self._last_no_boost_reported = 0 + self._all_boost_proposals = None + self._all_markets_ready = False + + self._ev_loop = asyncio.get_event_loop() + self._main_task = None + + self._last_timestamp = 0 + self._status_report_interval = status_report_interval + self.add_markets([market_info_1.market]) + self._quote_eth_rate_fetch_loop_task = None + + self._rate_source = rate_source + + self._cancel_outdated_orders_task = None + self._gateway_transaction_cancel_interval = gateway_transaction_cancel_interval + + @property + def all_markets_ready(self) -> bool: + return self._all_markets_ready + + @all_markets_ready.setter + def all_markets_ready(self, value: bool): + self._all_markets_ready = value + + @property + def order_amount(self) -> Decimal: + return self._order_amount + + @order_amount.setter + def order_amount(self, value: Decimal): + self._order_amount = value + + @property + def rate_source(self) -> Optional[RateOracle]: + return self._rate_source + + @rate_source.setter + def rate_source(self, src: Optional[RateOracle]): + self._rate_source = src + + @property + def market_info_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: + return self._sb_order_tracker.market_pair_to_active_orders + + @staticmethod + @lru_cache(maxsize=10) + def is_gateway_market(market_info: MarketTradingPairTuple) -> bool: + return market_info.market.name in sorted( + AllConnectorSettings.get_gateway_amm_connector_names() + ) + + @staticmethod + @lru_cache(maxsize=10) + def is_gateway_market_evm_compatible(market_info: MarketTradingPairTuple) -> bool: + connector_spec: Dict[str, str] = GatewayConnectionSetting.get_connector_spec_from_market_name(market_info.market.name) + return connector_spec["chain_type"] == "EVM" + + def tick(self, timestamp: float): + """ + Clock tick entry point, is run every second (on normal tick setting). + :param timestamp: current tick timestamp + """ + if not self.all_markets_ready: + self.all_markets_ready = all([market.ready for market in self.active_markets]) + if not self.all_markets_ready: + if int(timestamp) % 10 == 0: # prevent spamming by logging every 10 secs + unready_markets = [market for market in self.active_markets if market.ready is False] + for market in unready_markets: + msg = ', '.join([k for k, v in market.status_dict.items() if v is False]) + self.logger().warning(f"{market.name} not ready: waiting for {msg}.") + return + else: + self.logger().info("Markets are ready. Trading started.") + + if self.ready_for_new_boost_trades(): + if self._main_task is None or self._main_task.done(): + self._main_task = safe_ensure_future(self.main()) + # if self._cancel_outdated_orders_task is None or self._cancel_outdated_orders_task.done(): + # self._cancel_outdated_orders_task = safe_ensure_future(self.apply_gateway_transaction_cancel_interval()) + + async def main(self): + # Get price of the first market + market: GatewayEVMAMM = cast(GatewayEVMAMM, self._market_info_1.market) + slippage_buffer: Decimal = self._market_1_slippage_buffer + number_of_orders: int = self._number_of_orders + """ + Execute a boost volume trade. If trade completes, it will place another trade. Until all trades are completed. + """ + for i in range(number_of_orders): + slippage_buffer_factor: Decimal = Decimal(1) + slippage_buffer + # BUY + price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, True, self._order_amount, ignore_shim=True) + price_1 = price_1.quantize(Decimal("0.000000001")) + price_1 *= slippage_buffer_factor + await self.place_boost_order(self._market_info_1, True, self._order_amount, price_1) + await asyncio.sleep(2) + # log done + self.log_with_clock(logging.INFO, f"Placed buy order {i + 1}/{number_of_orders}") + # SELL + slippage_buffer_factor = Decimal(1) - slippage_buffer + price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, False, self._order_amount, ignore_shim=True) + price_1 = price_1.quantize(Decimal("0.000000001")) + price_1 *= slippage_buffer_factor + await self.place_boost_order(self._market_info_1, False, self._order_amount, price_1) + # Sleep for 5 seconds before placing another order + await asyncio.sleep(2) + # log done + self.log_with_clock(logging.INFO, f"Placed sell order {i + 1}/{number_of_orders}") + + async def apply_gateway_transaction_cancel_interval(self): + # XXX (martin_kou): Concurrent cancellations are not supported before the nonce architecture is fixed. + # See: https://app.shortcut.com/coinalpha/story/24553/nonce-architecture-in-current-amm-trade-and-evm-approve-apis-is-incorrect-and-causes-trouble-with-concurrent-requests + gateway_connectors = [] + if self.is_gateway_market(self._market_info_1) and self.is_gateway_market_evm_compatible(self._market_info_1): + gateway_connectors.append(cast(GatewayEVMAMM, self._market_info_1.market)) + + for gateway in gateway_connectors: + await gateway.cancel_outdated_orders(self._gateway_transaction_cancel_interval) + + async def place_boost_order( + self, + market_info: MarketTradingPairTuple, + is_buy: bool, + amount: Decimal, + order_price: Decimal) -> str: + place_order_fn: Callable[[MarketTradingPairTuple, Decimal, OrderType, Decimal], str] = \ + cast(Callable, self.buy_with_specific_market if is_buy else self.sell_with_specific_market) + + # If I'm placing order under a gateway price shim, then the prices in the proposal are fake - I should fetch + # the real prices before I make the order on the gateway side. Otherwise, the orders are gonna fail because + # the limit price set for them will not match market prices. + if self.is_gateway_market(market_info): + slippage_buffer: Decimal = self._market_1_slippage_buffer + slippage_buffer_factor: Decimal = Decimal(1) + slippage_buffer + if not is_buy: + slippage_buffer_factor = Decimal(1) - slippage_buffer + market: GatewayEVMAMM = cast(GatewayEVMAMM, market_info.market) + if GatewayPriceShim.get_instance().has_price_shim( + market.connector_name, market.chain, market.network, market_info.trading_pair): + order_price = await market.get_order_price(market_info.trading_pair, is_buy, amount, ignore_shim=True) + order_price *= slippage_buffer_factor + + return place_order_fn(market_info, amount, market_info.market.get_taker_order_type(), order_price) + + def ready_for_new_boost_trades(self) -> bool: + """ + Returns True if there is no outstanding unfilled order. + """ + # outstanding_orders = self.market_info_to_active_orders.get(self._market_info, []) + for market_info in [self._market_info_1]: + if len(self.market_info_to_active_orders.get(market_info, [])) > 0: + return False + return True + + def did_complete_buy_order(self, order_completed_event: BuyOrderCompletedEvent): + self.set_order_completed(order_id=order_completed_event.order_id) + + market_info: MarketTradingPairTuple = self.order_tracker.get_market_pair_from_order_id( + order_completed_event.order_id + ) + log_msg: str = f"Buy order completed on {market_info.market.name}: {order_completed_event.order_id}." + if self.is_gateway_market(market_info): + log_msg += f" txHash: {order_completed_event.exchange_order_id}" + self.log_with_clock(logging.INFO, log_msg) + self.notify_hb_app_with_timestamp(f"Bought {order_completed_event.base_asset_amount:.8f} " + f"{order_completed_event.base_asset}-{order_completed_event.quote_asset} " + f"on {market_info.market.name}.") + + def did_complete_sell_order(self, order_completed_event: SellOrderCompletedEvent): + self.set_order_completed(order_id=order_completed_event.order_id) + + market_info: MarketTradingPairTuple = self.order_tracker.get_market_pair_from_order_id( + order_completed_event.order_id + ) + log_msg: str = f"Sell order completed on {market_info.market.name}: {order_completed_event.order_id}." + if self.is_gateway_market(market_info): + log_msg += f" txHash: {order_completed_event.exchange_order_id}" + self.log_with_clock(logging.INFO, log_msg) + self.notify_hb_app_with_timestamp(f"Sold {order_completed_event.base_asset_amount:.8f} " + f"{order_completed_event.base_asset}-{order_completed_event.quote_asset} " + f"on {market_info.market.name}.") + + def did_fail_order(self, order_failed_event: MarketOrderFailureEvent): + self.set_order_failed(order_id=order_failed_event.order_id) + + def did_cancel_order(self, cancelled_event: OrderCancelledEvent): + self.set_order_completed(order_id=cancelled_event.order_id) + + def did_expire_order(self, expired_event: OrderExpiredEvent): + self.set_order_completed(order_id=expired_event.order_id) + + @property + def tracked_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: + return self._sb_order_tracker.tracked_limit_orders + + @property + def tracked_market_orders(self) -> List[Tuple[ConnectorBase, MarketOrder]]: + return self._sb_order_tracker.tracked_market_orders + + def start(self, clock: Clock, timestamp: float): + super().start(clock, timestamp) + + def stop(self, clock: Clock): + if self._main_task is not None: + self._main_task.cancel() + self._main_task = None + super().stop(clock) diff --git a/hummingbot/strategy/boost_volume/boost_volume_config_map.py b/hummingbot/strategy/boost_volume/boost_volume_config_map.py new file mode 100644 index 00000000000..994f39a835d --- /dev/null +++ b/hummingbot/strategy/boost_volume/boost_volume_config_map.py @@ -0,0 +1,109 @@ +from decimal import Decimal + +from hummingbot.client.config.config_validators import ( + validate_bool, + validate_connector, + validate_decimal, + validate_int, + validate_market_trading_pair, +) +from hummingbot.client.config.config_var import ConfigVar +from hummingbot.client.settings import AllConnectorSettings, required_exchanges, requried_connector_trading_pairs + + +def exchange_on_validated(value: str) -> None: + required_exchanges.add(value) + + +def market_1_validator(value: str) -> None: + exchange = boost_volume_config_map["connector_1"].value + return validate_market_trading_pair(exchange, value) + + +def market_1_on_validated(value: str) -> None: + requried_connector_trading_pairs[boost_volume_config_map["connector_1"].value] = [value] + + +def market_1_prompt() -> str: + connector = boost_volume_config_map.get("connector_1").value + example = AllConnectorSettings.get_example_pairs().get(connector) + return "Enter the token trading pair you would like to trade on %s%s >>> " \ + % (connector, f" (e.g. {example})" if example else "") + + +def order_amount_prompt() -> str: + trading_pair = boost_volume_config_map["market_1"].value + base_asset, quote_asset = trading_pair.split("-") + return f"What is the amount of {base_asset} per order? >>> " + + +boost_volume_config_map = { + "strategy": ConfigVar( + key="strategy", + prompt="", + default="boost_volume"), + "connector_1": ConfigVar( + key="connector_1", + prompt="Enter your first connector (Exchange/AMM/CLOB) >>> ", + prompt_on_new=True, + validator=validate_connector, + on_validated=exchange_on_validated), + "market_1": ConfigVar( + key="market_1", + prompt=market_1_prompt, + prompt_on_new=True, + validator=market_1_validator, + on_validated=market_1_on_validated), + "order_amount": ConfigVar( + key="order_amount", + prompt=order_amount_prompt, + type_str="decimal", + validator=lambda v: validate_decimal(v, Decimal("0")), + prompt_on_new=True), + "market_1_slippage_buffer": ConfigVar( + key="market_1_slippage_buffer", + prompt="How much buffer do you want to add to the price to account for slippage for orders on the first market " + "(Enter 1 for 1%)? >>> ", + prompt_on_new=True, + default=lambda: Decimal(1) if boost_volume_config_map["connector_1"].value in sorted( + AllConnectorSettings.get_gateway_amm_connector_names().union( + AllConnectorSettings.get_gateway_clob_connector_names() + ) + ) else Decimal(0), + validator=lambda v: validate_decimal(v), + type_str="decimal"), + "number_of_orders": ConfigVar( + key="number_of_orders", + prompt="How many orders do you want to place? >>> ", + prompt_on_new=True, + default=1, + validator=lambda v: validate_int(v, min_value=1, inclusive=True), + type_str="int" + ), + "debug_price_shim": ConfigVar( + key="debug_price_shim", + prompt="Do you want to enable the debug price shim for integration tests? If you don't know what this does " + "you should keep it disabled. >>> ", + default=False, + validator=validate_bool, + type_str="bool"), + "gateway_transaction_cancel_interval": ConfigVar( + key="gateway_transaction_cancel_interval", + prompt="After what time should blockchain transactions be cancelled if they are not included in a block? " + "(this only affects decentralized exchanges) (Enter time in seconds) >>> ", + default=600, + validator=lambda v: validate_int(v, min_value=1, inclusive=True), + type_str="int"), + "rate_oracle_enabled": ConfigVar( + key="rate_oracle_enabled", + prompt="Do you want to use the rate oracle? (Yes/No) >>> ", + default=True, + validator=validate_bool, + type_str="bool"), + "quote_conversion_rate": ConfigVar( + key="quote_conversion_rate", + prompt="What is the fixed_rate used to convert quote assets? >>> ", + default=Decimal("1"), + validator=lambda v: validate_decimal(v), + type_str="decimal"), +} diff --git a/hummingbot/strategy/boost_volume/start.py b/hummingbot/strategy/boost_volume/start.py new file mode 100644 index 00000000000..0be08e59ce5 --- /dev/null +++ b/hummingbot/strategy/boost_volume/start.py @@ -0,0 +1,51 @@ +from decimal import Decimal + +# from hummingbot.connector.gateway.amm.gateway_evm_amm import GatewayEVMAMM +# from hummingbot.connector.gateway.amm.gateway_tezos_amm import GatewayTezosAMM +# from hummingbot.connector.gateway.common_types import Chain +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.core.utils.fixed_rate_source import FixedRateSource +from hummingbot.strategy.boost_volume.boost_volume import BoostVolumeStrategy +from hummingbot.strategy.boost_volume.boost_volume_config_map import boost_volume_config_map +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple + + +# from typing import cast +def start(self): + connector_1 = boost_volume_config_map.get("connector_1").value.lower() + market_1 = boost_volume_config_map.get("market_1").value + order_amount = boost_volume_config_map.get("order_amount").value + market_1_slippage_buffer = boost_volume_config_map.get("market_1_slippage_buffer").value / Decimal("100") + # debug_price_shim = boost_volume_config_map.get("debug_price_shim").value + gateway_transaction_cancel_interval = boost_volume_config_map.get("gateway_transaction_cancel_interval").value + rate_oracle_enabled = boost_volume_config_map.get("rate_oracle_enabled").value + quote_conversion_rate = boost_volume_config_map.get("quote_conversion_rate").value + + self._initialize_markets([(connector_1, [market_1])]) + base_1, quote_1 = market_1.split("-") + + market_info_1 = MarketTradingPairTuple(self.markets[connector_1], market_1, base_1, quote_1) + self.market_trading_pair_tuples = [market_info_1] + + # if debug_price_shim: + # amm_market_info: MarketTradingPairTuple = market_info_1 + # if Chain.ETHEREUM.chain == amm_market_info.market.chain: + # amm_connector: GatewayEVMAMM = cast(GatewayEVMAMM, amm_market_info.market) + # elif Chain.TEZOS.chain == amm_market_info.market.chain: + # amm_connector: GatewayTezosAMM = cast(GatewayTezosAMM, amm_market_info.market) + # else: + # raise ValueError(f"Unsupported chain: {amm_market_info.market.chain}") + + if rate_oracle_enabled: + rate_source = RateOracle.get_instance() + else: + rate_source = FixedRateSource() + rate_source.add_rate(f"{quote_1}-{quote_1}", Decimal(str(quote_conversion_rate))) # reverse rate is already handled in FixedRateSource find_rate method. + + self.strategy = BoostVolumeStrategy() + self.strategy.init_params(market_info_1=market_info_1, + order_amount=order_amount, + market_1_slippage_buffer=market_1_slippage_buffer, + gateway_transaction_cancel_interval=gateway_transaction_cancel_interval, + rate_source=rate_source, + ) diff --git a/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml b/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml new file mode 100644 index 00000000000..e8e676baf9f --- /dev/null +++ b/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml @@ -0,0 +1,36 @@ +########################################## +### Increase Trading Volume strategy config ### +########################################## + +template_version: 6 +strategy: null + +# The following configurations are only required for the Increase Trading Volume trading strategy + +# Connectors and markets parameters +connector_1: null +market_1: null + +order_amount: null + +# A buffer for which to adjust order price for higher chance of the order getting filled. +# This is important for AMM which transaction takes a long time where a slippage is acceptable rather having +# the transaction get rejected. The submitted order price will be adjust higher (by percentage value) for buy order +# and lower for sell order. (Enter 1 for 1%) +market_1_slippage_buffer: null + +# Number of orders to place +number_of_orders: null + +# A flag (true/false), if true would enable a price shim feature on the AMM connector to allow developer to simulate +# different prices on the AMM market. This is used for development and testing purpose only. +debug_price_shim: false + +# After how many seconds should blockchain transactions be cancelled if they are not included in a block? +gateway_transaction_cancel_interval: 600 + +# What rate source should be used for quote assets pair - between fixed_rate_source and rate_oracle_source? +rate_oracle_enabled: true + +# What is the fixed_rate used to convert quote assets? +quote_conversion_rate: 1 \ No newline at end of file From 6a03fe464a2d0f945548ffe423410cd5c32fd65e Mon Sep 17 00:00:00 2001 From: haitranwang Date: Mon, 29 Jul 2024 12:24:13 +0700 Subject: [PATCH 3/4] Add number_of_orders attribute --- hummingbot/strategy/boost_volume/boost_volume.py | 2 +- hummingbot/strategy/boost_volume/start.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hummingbot/strategy/boost_volume/boost_volume.py b/hummingbot/strategy/boost_volume/boost_volume.py index cb96a5625b9..49108f8cb82 100644 --- a/hummingbot/strategy/boost_volume/boost_volume.py +++ b/hummingbot/strategy/boost_volume/boost_volume.py @@ -62,7 +62,7 @@ def init_params(self, market_info_1: MarketTradingPairTuple, order_amount: Decimal, market_1_slippage_buffer: Decimal = Decimal("0"), - number_of_orders: int = 10, + number_of_orders: int = 0, status_report_interval: float = 900, gateway_transaction_cancel_interval: int = 600, rate_source: Optional[RateOracle] = RateOracle.get_instance(), diff --git a/hummingbot/strategy/boost_volume/start.py b/hummingbot/strategy/boost_volume/start.py index 0be08e59ce5..58a658b66e9 100644 --- a/hummingbot/strategy/boost_volume/start.py +++ b/hummingbot/strategy/boost_volume/start.py @@ -16,6 +16,7 @@ def start(self): market_1 = boost_volume_config_map.get("market_1").value order_amount = boost_volume_config_map.get("order_amount").value market_1_slippage_buffer = boost_volume_config_map.get("market_1_slippage_buffer").value / Decimal("100") + number_of_orders = boost_volume_config_map.get("number_of_orders").value # debug_price_shim = boost_volume_config_map.get("debug_price_shim").value gateway_transaction_cancel_interval = boost_volume_config_map.get("gateway_transaction_cancel_interval").value rate_oracle_enabled = boost_volume_config_map.get("rate_oracle_enabled").value @@ -46,6 +47,7 @@ def start(self): self.strategy.init_params(market_info_1=market_info_1, order_amount=order_amount, market_1_slippage_buffer=market_1_slippage_buffer, + number_of_orders=number_of_orders, gateway_transaction_cancel_interval=gateway_transaction_cancel_interval, rate_source=rate_source, ) From 90d8afc32d47be8c537404c3264b11fc3551c1b7 Mon Sep 17 00:00:00 2001 From: haitranwang Date: Fri, 9 Aug 2024 11:12:11 +0700 Subject: [PATCH 4/4] update boost volume and first commit for copy trade strategy --- .../strategy/boost_volume/boost_volume.py | 83 +++-- .../boost_volume/boost_volume_config_map.py | 36 ++- hummingbot/strategy/boost_volume/start.py | 10 +- hummingbot/strategy/copy_trade/__init__.py | 0 hummingbot/strategy/copy_trade/copy_trade.py | 304 ++++++++++++++++++ .../copy_trade/copy_trade_config_map.py | 138 ++++++++ hummingbot/strategy/copy_trade/start.py | 46 +++ .../conf_boost_volume_strategy_TEMPLATE.yml | 12 +- .../conf_copy_trade_strategy_TEMPLATE.yml | 40 +++ 9 files changed, 640 insertions(+), 29 deletions(-) create mode 100644 hummingbot/strategy/copy_trade/__init__.py create mode 100644 hummingbot/strategy/copy_trade/copy_trade.py create mode 100644 hummingbot/strategy/copy_trade/copy_trade_config_map.py create mode 100644 hummingbot/strategy/copy_trade/start.py create mode 100644 hummingbot/templates/conf_copy_trade_strategy_TEMPLATE.yml diff --git a/hummingbot/strategy/boost_volume/boost_volume.py b/hummingbot/strategy/boost_volume/boost_volume.py index 49108f8cb82..c22bbca04e6 100644 --- a/hummingbot/strategy/boost_volume/boost_volume.py +++ b/hummingbot/strategy/boost_volume/boost_volume.py @@ -1,5 +1,6 @@ import asyncio import logging +import random from decimal import Decimal from functools import lru_cache from typing import Callable, Dict, List, Optional, Tuple, cast @@ -36,7 +37,10 @@ class BoostVolumeStrategy(StrategyPyBase): """ _market_info_1: MarketTradingPairTuple - _order_amount: Decimal + _order_amount_from: Decimal + _order_amount_to: Decimal + _delay_from: int + _delay_range: int _market_1_slippage_buffer: Decimal _number_of_orders: int _last_no_boost_reported: float @@ -60,7 +64,10 @@ def logger(cls) -> HummingbotLogger: def init_params(self, market_info_1: MarketTradingPairTuple, - order_amount: Decimal, + order_amount_from: Decimal, + order_amount_to: Decimal, + delay_from: int, + delay_range: int, market_1_slippage_buffer: Decimal = Decimal("0"), number_of_orders: int = 0, status_report_interval: float = 900, @@ -69,7 +76,10 @@ def init_params(self, ): self._market_info_1 = market_info_1 - self._order_amount = order_amount + self._order_amount_from = order_amount_from + self._order_amount_to = order_amount_to + self._delay_from = delay_from + self._delay_range = delay_range self._market_1_slippage_buffer = market_1_slippage_buffer self._number_of_orders = number_of_orders self._last_no_boost_reported = 0 @@ -98,12 +108,36 @@ def all_markets_ready(self, value: bool): self._all_markets_ready = value @property - def order_amount(self) -> Decimal: - return self._order_amount + def order_amount_from(self) -> Decimal: + return self._order_amount_from - @order_amount.setter - def order_amount(self, value: Decimal): - self._order_amount = value + @order_amount_from.setter + def order_amount_from(self, value: Decimal): + self._order_amount_from = value + + @property + def order_amount_to(self) -> Decimal: + return self._order_amount_to + + @order_amount_to.setter + def order_amount_to(self, value: Decimal): + self._order_amount_to = value + + @property + def delay_from(self) -> int: + return self._delay_from + + @delay_from.setter + def delay_from(self, value: int): + self._delay_from = value + + @property + def delay_range(self) -> int: + return self._delay_range + + @delay_range.setter + def delay_range(self, value: int): + self._delay_range = value @property def rate_source(self) -> Optional[RateOracle]: @@ -150,8 +184,6 @@ def tick(self, timestamp: float): if self.ready_for_new_boost_trades(): if self._main_task is None or self._main_task.done(): self._main_task = safe_ensure_future(self.main()) - # if self._cancel_outdated_orders_task is None or self._cancel_outdated_orders_task.done(): - # self._cancel_outdated_orders_task = safe_ensure_future(self.apply_gateway_transaction_cancel_interval()) async def main(self): # Get price of the first market @@ -164,23 +196,32 @@ async def main(self): for i in range(number_of_orders): slippage_buffer_factor: Decimal = Decimal(1) + slippage_buffer # BUY - price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, True, self._order_amount, ignore_shim=True) - price_1 = price_1.quantize(Decimal("0.000000001")) + random_amount_buy: Decimal = Decimal(round(random.uniform(float(self._order_amount_from), float(self._order_amount_to)), 5)) + random_delay_value_buy: int = random.randint(self._delay_from, self._delay_from + self._delay_range) + price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, True, random_amount_buy, ignore_shim=True) + price_1 = price_1.quantize(Decimal("0.00000001")) price_1 *= slippage_buffer_factor - await self.place_boost_order(self._market_info_1, True, self._order_amount, price_1) - await asyncio.sleep(2) + print(f"random_amount_buy: {type(random_amount_buy)}") + print(f"price_1: {type(price_1)}") + print(f"random_delay_value_buy: {random_delay_value_buy}") + await self.place_boost_order(self._market_info_1, True, random_amount_buy, price_1) + await asyncio.sleep(random_delay_value_buy) # log done - self.log_with_clock(logging.INFO, f"Placed buy order {i + 1}/{number_of_orders}") + self.log_with_clock(logging.INFO, f"Placed buy order {i + 1}/{number_of_orders}: {random_amount_buy} AURA") # SELL + random_amount_sell: Decimal = Decimal(round(random.uniform(float(self._order_amount_from), float(self._order_amount_to)), 5)) + random_delay_value_sell: int = random.randint(self._delay_from, self._delay_from + self._delay_range) slippage_buffer_factor = Decimal(1) - slippage_buffer - price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, False, self._order_amount, ignore_shim=True) - price_1 = price_1.quantize(Decimal("0.000000001")) + price_1: Decimal = await market.get_order_price(self._market_info_1.trading_pair, False, random_amount_sell, ignore_shim=True) + price_1 = price_1.quantize(Decimal("0.00000001")) price_1 *= slippage_buffer_factor - await self.place_boost_order(self._market_info_1, False, self._order_amount, price_1) - # Sleep for 5 seconds before placing another order - await asyncio.sleep(2) + print(f"random_amount_sell: {random_amount_sell}") + print(f"price_1: {price_1}") + print(f"random_delay_value_sell: {random_delay_value_sell}") + await self.place_boost_order(self._market_info_1, False, random_amount_sell, price_1) + await asyncio.sleep(random_delay_value_sell) # log done - self.log_with_clock(logging.INFO, f"Placed sell order {i + 1}/{number_of_orders}") + self.log_with_clock(logging.INFO, f"Placed sell order {i + 1}/{number_of_orders}: {random_amount_sell} AURA") async def apply_gateway_transaction_cancel_interval(self): # XXX (martin_kou): Concurrent cancellations are not supported before the nonce architecture is fixed. diff --git a/hummingbot/strategy/boost_volume/boost_volume_config_map.py b/hummingbot/strategy/boost_volume/boost_volume_config_map.py index 994f39a835d..2ab5d624fef 100644 --- a/hummingbot/strategy/boost_volume/boost_volume_config_map.py +++ b/hummingbot/strategy/boost_volume/boost_volume_config_map.py @@ -31,10 +31,16 @@ def market_1_prompt() -> str: % (connector, f" (e.g. {example})" if example else "") -def order_amount_prompt() -> str: +def order_amount_from_prompt() -> str: trading_pair = boost_volume_config_map["market_1"].value base_asset, quote_asset = trading_pair.split("-") - return f"What is the amount of {base_asset} per order? >>> " + return f"What is the minimum amount of {base_asset} per order? >>> " + + +def order_amount_to_prompt() -> str: + trading_pair = boost_volume_config_map["market_1"].value + base_asset, quote_asset = trading_pair.split("-") + return f"What is the maximum amount of {base_asset} per order? >>> " boost_volume_config_map = { @@ -54,12 +60,32 @@ def order_amount_prompt() -> str: prompt_on_new=True, validator=market_1_validator, on_validated=market_1_on_validated), - "order_amount": ConfigVar( - key="order_amount", - prompt=order_amount_prompt, + "order_amount_from": ConfigVar( + key="order_amount_from", + prompt=order_amount_from_prompt, type_str="decimal", validator=lambda v: validate_decimal(v, Decimal("0")), prompt_on_new=True), + "order_amount_to": ConfigVar( + key="order_amount_to", + prompt=order_amount_to_prompt, + type_str="decimal", + validator=lambda v: validate_decimal(v, Decimal("0")), + prompt_on_new=True), + "delay_from": ConfigVar( + key="delay_from", + prompt="What is the minimum delay between orders? (Enter time in seconds) >>> ", + default=0, + validator=lambda v: validate_int(v, min_value=0, inclusive=True), + type_str="int", + prompt_on_new=True), + "delay_range": ConfigVar( + key="delay_range", + prompt="What is the range of delay? (Enter time in seconds) >>> ", + default=0, + validator=lambda v: validate_int(v, min_value=0, inclusive=True), + type_str="int", + prompt_on_new=True), "market_1_slippage_buffer": ConfigVar( key="market_1_slippage_buffer", prompt="How much buffer do you want to add to the price to account for slippage for orders on the first market " diff --git a/hummingbot/strategy/boost_volume/start.py b/hummingbot/strategy/boost_volume/start.py index 58a658b66e9..34992dae3aa 100644 --- a/hummingbot/strategy/boost_volume/start.py +++ b/hummingbot/strategy/boost_volume/start.py @@ -14,7 +14,10 @@ def start(self): connector_1 = boost_volume_config_map.get("connector_1").value.lower() market_1 = boost_volume_config_map.get("market_1").value - order_amount = boost_volume_config_map.get("order_amount").value + order_amount_from = boost_volume_config_map.get("order_amount_from").value + order_amount_to = boost_volume_config_map.get("order_amount_to").value + delay_from = boost_volume_config_map.get("delay_from").value + delay_range = boost_volume_config_map.get("delay_range").value market_1_slippage_buffer = boost_volume_config_map.get("market_1_slippage_buffer").value / Decimal("100") number_of_orders = boost_volume_config_map.get("number_of_orders").value # debug_price_shim = boost_volume_config_map.get("debug_price_shim").value @@ -45,7 +48,10 @@ def start(self): self.strategy = BoostVolumeStrategy() self.strategy.init_params(market_info_1=market_info_1, - order_amount=order_amount, + order_amount_from=order_amount_from, + order_amount_to=order_amount_to, + delay_from=delay_from, + delay_range=delay_range, market_1_slippage_buffer=market_1_slippage_buffer, number_of_orders=number_of_orders, gateway_transaction_cancel_interval=gateway_transaction_cancel_interval, diff --git a/hummingbot/strategy/copy_trade/__init__.py b/hummingbot/strategy/copy_trade/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/hummingbot/strategy/copy_trade/copy_trade.py b/hummingbot/strategy/copy_trade/copy_trade.py new file mode 100644 index 00000000000..59bcfae2fa3 --- /dev/null +++ b/hummingbot/strategy/copy_trade/copy_trade.py @@ -0,0 +1,304 @@ +import asyncio +import logging +from decimal import Decimal +from functools import lru_cache +from typing import Callable, Dict, List, Optional, Tuple, cast + +import pymongo + +from hummingbot.client.settings import AllConnectorSettings, GatewayConnectionSetting +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.connector.gateway.amm.gateway_evm_amm import GatewayEVMAMM +from hummingbot.connector.gateway.gateway_price_shim import GatewayPriceShim +from hummingbot.core.clock import Clock +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.data_type.market_order import MarketOrder +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + MarketOrderFailureEvent, + OrderCancelledEvent, + OrderExpiredEvent, + OrderType, + SellOrderCompletedEvent, +) +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.logger import HummingbotLogger +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.strategy_py_base import StrategyPyBase + +NaN = float("nan") +s_decimal_zero = Decimal(0) +amm_logger = None + + +class CopyTradeStrategy(StrategyPyBase): + """ + This is a basic copying strategy which can be used for most types of connectors (CEX, DEX or AMM). + """ + + _market_info_1: MarketTradingPairTuple + _wallet_to_copy: str + _type_of_copy: str + _percentage: Decimal + _fixed_amount: Decimal + _market_1_slippage_buffer: Decimal + _last_no_copy_reported: float + _all_markets_ready: bool + _ev_loop: asyncio.AbstractEventLoop + _main_task: Optional[asyncio.Task] + _last_timestamp: float + _status_report_interval: float + _quote_eth_rate_fetch_loop_task: Optional[asyncio.Task] + _market_1_quote_eth_rate: None # XXX (martin_kou): Why are these here? + _rate_source: Optional[RateOracle] + _cancel_outdated_orders_task: Optional[asyncio.Task] + _gateway_transaction_cancel_interval: int + + @classmethod + def logger(cls) -> HummingbotLogger: + global amm_logger + if amm_logger is None: + amm_logger = logging.getLogger(__name__) + return amm_logger + + def init_params(self, + market_info_1: MarketTradingPairTuple, + wallet_to_copy: str, + type_of_copy: str, + percentage: Decimal, + fixed_amount: Decimal, + market_1_slippage_buffer: Decimal = Decimal("0"), + number_of_orders: int = 0, + status_report_interval: float = 900, + gateway_transaction_cancel_interval: int = 600, + rate_source: Optional[RateOracle] = RateOracle.get_instance(), + ): + + self._market_info_1 = market_info_1 + self._wallet_to_copy = wallet_to_copy + self._type_of_copy = type_of_copy + self._percentage = percentage + self._fixed_amount = fixed_amount + self._market_1_slippage_buffer = market_1_slippage_buffer + self._number_of_orders = number_of_orders + self._last_no_copy_reported = 0 + self._all_copy_proposals = None + self._all_markets_ready = False + + self._ev_loop = asyncio.get_event_loop() + self._main_task = None + + self._last_timestamp = 0 + self._status_report_interval = status_report_interval + self.add_markets([market_info_1.market]) + self._quote_eth_rate_fetch_loop_task = None + + self._rate_source = rate_source + + self._cancel_outdated_orders_task = None + self._gateway_transaction_cancel_interval = gateway_transaction_cancel_interval + + @property + def all_markets_ready(self) -> bool: + return self._all_markets_ready + + @all_markets_ready.setter + def all_markets_ready(self, value: bool): + self._all_markets_ready = value + + @property + def wallet_to_copy(self) -> str: + return self._wallet_to_copy + + @wallet_to_copy.setter + def wallet_to_copy(self, value: str): + self._wallet_to_copy = value + + @property + def type_of_copy(self) -> str: + return self._type_of_copy + + @type_of_copy.setter + def type_of_copy(self, value: str): + self._type_of_copy = value + + @property + def percentage(self) -> Decimal: + return self._percentage + + @percentage.setter + def percentage(self, value: Decimal): + self._percentage = value + + @property + def fixed_amount(self) -> Decimal: + return self._fixed_amount + + @fixed_amount.setter + def fixed_amount(self, value: Decimal): + self._fixed_amount = value + + @property + def rate_source(self) -> Optional[RateOracle]: + return self._rate_source + + @rate_source.setter + def rate_source(self, src: Optional[RateOracle]): + self._rate_source = src + + @property + def market_info_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: + return self._sb_order_tracker.market_pair_to_active_orders + + @staticmethod + @lru_cache(maxsize=10) + def is_gateway_market(market_info: MarketTradingPairTuple) -> bool: + return market_info.market.name in sorted( + AllConnectorSettings.get_gateway_amm_connector_names() + ) + + @staticmethod + @lru_cache(maxsize=10) + def is_gateway_market_evm_compatible(market_info: MarketTradingPairTuple) -> bool: + connector_spec: Dict[str, str] = GatewayConnectionSetting.get_connector_spec_from_market_name(market_info.market.name) + return connector_spec["chain_type"] == "EVM" + + def tick(self, timestamp: float): + """ + Clock tick entry point, is run every second (on normal tick setting). + :param timestamp: current tick timestamp + """ + print("tickkkkkk") + if not self.all_markets_ready: + print("not ready") + self.all_markets_ready = all([market.ready for market in self.active_markets]) + if not self.all_markets_ready: + if int(timestamp) % 10 == 0: # prevent spamming by logging every 10 secs + unready_markets = [market for market in self.active_markets if market.ready is False] + for market in unready_markets: + msg = ', '.join([k for k, v in market.status_dict.items() if v is False]) + self.logger().warning(f"{market.name} not ready: waiting for {msg}.") + return + else: + self.logger().info("Markets are ready. Trading started.") + + if self.ready_for_new_copy_trade_trades(): + if self._main_task is None or self._main_task.done(): + self._main_task = safe_ensure_future(self.main()) + + async def main(self): + # Get price of the first market + # market: GatewayEVMAMM = cast(GatewayEVMAMM, self._market_info_1.market) + # slippage_buffer: Decimal = self._market_1_slippage_buffer + # number_of_orders: int = self._number_of_orders + """ + Execute a copy trade trade. If trade completes, it will place another trade. Until all trades are completed. + """ + myclient = pymongo.MongoClient("mongodb+srv://haitranwang:z0PkCWCQPDrVo3LW@cluster0.j82ajdm.mongodb.net") + mydb = myclient["test"] + mycol = mydb["transactions"] + myquery = {"from": "aura1vqu8rska6swzdmnhf90zuv0xmelej4lqkyjkk3"} + mydoc = mycol.find(myquery) + + for x in mydoc: + print(x) + + async def apply_gateway_transaction_cancel_interval(self): + # XXX (martin_kou): Concurrent cancellations are not supported before the nonce architecture is fixed. + # See: https://app.shortcut.com/coinalpha/story/24553/nonce-architecture-in-current-amm-trade-and-evm-approve-apis-is-incorrect-and-causes-trouble-with-concurrent-requests + gateway_connectors = [] + if self.is_gateway_market(self._market_info_1) and self.is_gateway_market_evm_compatible(self._market_info_1): + gateway_connectors.append(cast(GatewayEVMAMM, self._market_info_1.market)) + + for gateway in gateway_connectors: + await gateway.cancel_outdated_orders(self._gateway_transaction_cancel_interval) + + async def place_copy_order( + self, + market_info: MarketTradingPairTuple, + is_buy: bool, + amount: Decimal, + order_price: Decimal) -> str: + place_order_fn: Callable[[MarketTradingPairTuple, Decimal, OrderType, Decimal], str] = \ + cast(Callable, self.buy_with_specific_market if is_buy else self.sell_with_specific_market) + + # If I'm placing order under a gateway price shim, then the prices in the proposal are fake - I should fetch + # the real prices before I make the order on the gateway side. Otherwise, the orders are gonna fail because + # the limit price set for them will not match market prices. + if self.is_gateway_market(market_info): + slippage_buffer: Decimal = self._market_1_slippage_buffer + slippage_buffer_factor: Decimal = Decimal(1) + slippage_buffer + if not is_buy: + slippage_buffer_factor = Decimal(1) - slippage_buffer + market: GatewayEVMAMM = cast(GatewayEVMAMM, market_info.market) + if GatewayPriceShim.get_instance().has_price_shim( + market.connector_name, market.chain, market.network, market_info.trading_pair): + order_price = await market.get_order_price(market_info.trading_pair, is_buy, amount, ignore_shim=True) + order_price *= slippage_buffer_factor + + return place_order_fn(market_info, amount, market_info.market.get_taker_order_type(), order_price) + + def ready_for_new_copy_trade_trades(self) -> bool: + """ + Returns True if there is no outstanding unfilled order. + """ + # outstanding_orders = self.market_info_to_active_orders.get(self._market_info, []) + for market_info in [self._market_info_1]: + if len(self.market_info_to_active_orders.get(market_info, [])) > 0: + return False + return True + + def did_complete_buy_order(self, order_completed_event: BuyOrderCompletedEvent): + self.set_order_completed(order_id=order_completed_event.order_id) + + market_info: MarketTradingPairTuple = self.order_tracker.get_market_pair_from_order_id( + order_completed_event.order_id + ) + log_msg: str = f"Buy order completed on {market_info.market.name}: {order_completed_event.order_id}." + if self.is_gateway_market(market_info): + log_msg += f" txHash: {order_completed_event.exchange_order_id}" + self.log_with_clock(logging.INFO, log_msg) + self.notify_hb_app_with_timestamp(f"Bought {order_completed_event.base_asset_amount:.8f} " + f"{order_completed_event.base_asset}-{order_completed_event.quote_asset} " + f"on {market_info.market.name}.") + + def did_complete_sell_order(self, order_completed_event: SellOrderCompletedEvent): + self.set_order_completed(order_id=order_completed_event.order_id) + + market_info: MarketTradingPairTuple = self.order_tracker.get_market_pair_from_order_id( + order_completed_event.order_id + ) + log_msg: str = f"Sell order completed on {market_info.market.name}: {order_completed_event.order_id}." + if self.is_gateway_market(market_info): + log_msg += f" txHash: {order_completed_event.exchange_order_id}" + self.log_with_clock(logging.INFO, log_msg) + self.notify_hb_app_with_timestamp(f"Sold {order_completed_event.base_asset_amount:.8f} " + f"{order_completed_event.base_asset}-{order_completed_event.quote_asset} " + f"on {market_info.market.name}.") + + def did_fail_order(self, order_failed_event: MarketOrderFailureEvent): + self.set_order_failed(order_id=order_failed_event.order_id) + + def did_cancel_order(self, cancelled_event: OrderCancelledEvent): + self.set_order_completed(order_id=cancelled_event.order_id) + + def did_expire_order(self, expired_event: OrderExpiredEvent): + self.set_order_completed(order_id=expired_event.order_id) + + @property + def tracked_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: + return self._sb_order_tracker.tracked_limit_orders + + @property + def tracked_market_orders(self) -> List[Tuple[ConnectorBase, MarketOrder]]: + return self._sb_order_tracker.tracked_market_orders + + def start(self, clock: Clock, timestamp: float): + super().start(clock, timestamp) + + def stop(self, clock: Clock): + if self._main_task is not None: + self._main_task.cancel() + self._main_task = None + super().stop(clock) diff --git a/hummingbot/strategy/copy_trade/copy_trade_config_map.py b/hummingbot/strategy/copy_trade/copy_trade_config_map.py new file mode 100644 index 00000000000..833467b9bcd --- /dev/null +++ b/hummingbot/strategy/copy_trade/copy_trade_config_map.py @@ -0,0 +1,138 @@ +from decimal import Decimal + +from hummingbot.client.config.config_validators import ( + validate_bool, + validate_connector, + validate_decimal, + validate_int, + validate_market_trading_pair, +) +from hummingbot.client.config.config_var import ConfigVar +from hummingbot.client.settings import AllConnectorSettings, required_exchanges, requried_connector_trading_pairs + + +def exchange_on_validated(value: str) -> None: + required_exchanges.add(value) + + +def market_1_validator(value: str) -> None: + exchange = copy_trade_config_map["connector_1"].value + return validate_market_trading_pair(exchange, value) + + +def validate_ethereum_wallet_address(value: str) -> None: + if not value.startswith("0x") or len(value) != 42: + raise ValueError("Invalid Ethereum wallet address.") + + +def validate_type_of_copy(value: str) -> None: + if value not in {"percentage", "fixed_amount"}: + raise ValueError("Invalid type of copy.") + + +def market_1_on_validated(value: str) -> None: + requried_connector_trading_pairs[copy_trade_config_map["connector_1"].value] = [value] + + +def market_1_prompt() -> str: + connector = copy_trade_config_map.get("connector_1").value + example = AllConnectorSettings.get_example_pairs().get(connector) + return "Enter the token trading pair you would like to trade on %s%s >>> " \ + % (connector, f" (e.g. {example})" if example else "") + + +def order_amount_from_prompt() -> str: + trading_pair = copy_trade_config_map["market_1"].value + base_asset, quote_asset = trading_pair.split("-") + return f"What is the minimum amount of {base_asset} per order? >>> " + + +def order_amount_to_prompt() -> str: + trading_pair = copy_trade_config_map["market_1"].value + base_asset, quote_asset = trading_pair.split("-") + return f"What is the maximum amount of {base_asset} per order? >>> " + + +copy_trade_config_map = { + "strategy": ConfigVar( + key="strategy", + prompt="", + default="copy_trade"), + "connector_1": ConfigVar( + key="connector_1", + prompt="Enter your first connector (Exchange/AMM/CLOB) >>> ", + prompt_on_new=True, + validator=validate_connector, + on_validated=exchange_on_validated), + "market_1": ConfigVar( + key="market_1", + prompt=market_1_prompt, + prompt_on_new=True, + validator=market_1_validator, + on_validated=market_1_on_validated), + "wallet_to_copy": ConfigVar( + key="wallet_to_copy", + prompt="Enter the wallet address to copy trades >>> ", + prompt_on_new=True, + validator=validate_ethereum_wallet_address, + type_str="str"), + "type_of_copy": ConfigVar( + key="type_of_copy", + prompt="Enter the type of copy (percentage/fixed_amount) >>> ", + prompt_on_new=True, + validator= validate_type_of_copy), + "percentage": ConfigVar( + key="percentage", + prompt="Enter the percentage of the trade to copy (Enter 1 for 1%) >>> ", + prompt_on_new=True, + required_if=lambda: copy_trade_config_map.get("type_of_copy").value == "percentage", + default=Decimal(0), + validator=lambda v: validate_decimal(v), + type_str="decimal"), + "fixed_amount": ConfigVar( + key="copy_fixed_amount", + prompt="Enter the fixed amount of the trade to copy >>> ", + prompt_on_new=True, + required_if=lambda: copy_trade_config_map.get("type_of_copy").value == "fixed_amount", + default=Decimal(0), + validator=lambda v: validate_decimal(v), + type_str="decimal"), + "market_1_slippage_buffer": ConfigVar( + key="market_1_slippage_buffer", + prompt="How much buffer do you want to add to the price to account for slippage for orders on the first market " + "(Enter 1 for 1%)? >>> ", + prompt_on_new=True, + default=lambda: Decimal(1) if copy_trade_config_map["connector_1"].value in sorted( + AllConnectorSettings.get_gateway_amm_connector_names().union( + AllConnectorSettings.get_gateway_clob_connector_names() + ) + ) else Decimal(0), + validator=lambda v: validate_decimal(v), + type_str="decimal"), + "debug_price_shim": ConfigVar( + key="debug_price_shim", + prompt="Do you want to enable the debug price shim for integration tests? If you don't know what this does " + "you should keep it disabled. >>> ", + default=False, + validator=validate_bool, + type_str="bool"), + "gateway_transaction_cancel_interval": ConfigVar( + key="gateway_transaction_cancel_interval", + prompt="After what time should blockchain transactions be cancelled if they are not included in a block? " + "(this only affects decentralized exchanges) (Enter time in seconds) >>> ", + default=600, + validator=lambda v: validate_int(v, min_value=1, inclusive=True), + type_str="int"), + "rate_oracle_enabled": ConfigVar( + key="rate_oracle_enabled", + prompt="Do you want to use the rate oracle? (Yes/No) >>> ", + default=False, + validator=validate_bool, + type_str="bool"), + "quote_conversion_rate": ConfigVar( + key="quote_conversion_rate", + prompt="What is the fixed_rate used to convert quote assets? >>> ", + default=Decimal("1"), + validator=lambda v: validate_decimal(v), + type_str="decimal"), +} diff --git a/hummingbot/strategy/copy_trade/start.py b/hummingbot/strategy/copy_trade/start.py new file mode 100644 index 00000000000..92ec23131c0 --- /dev/null +++ b/hummingbot/strategy/copy_trade/start.py @@ -0,0 +1,46 @@ +from decimal import Decimal + +# from hummingbot.connector.gateway.amm.gateway_evm_amm import GatewayEVMAMM +# from hummingbot.connector.gateway.amm.gateway_tezos_amm import GatewayTezosAMM +# from hummingbot.connector.gateway.common_types import Chain +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.core.utils.fixed_rate_source import FixedRateSource +from hummingbot.strategy.copy_trade.copy_trade import CopyTradeStrategy +from hummingbot.strategy.copy_trade.copy_trade_config_map import copy_trade_config_map +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple + + +# from typing import cast +def start(self): + connector_1 = copy_trade_config_map.get("connector_1").value.lower() + market_1 = copy_trade_config_map.get("market_1").value + wallet_to_copy = copy_trade_config_map.get("wallet_to_copy").value + type_of_copy = copy_trade_config_map.get("type_of_copy").value + percentage = copy_trade_config_map.get("percentage").value + fixed_amount = copy_trade_config_map.get("fixed_amount").value + market_1_slippage_buffer = copy_trade_config_map.get("market_1_slippage_buffer").value / Decimal("100") + # debug_price_shim = copy_trade_config_map.get("debug_price_shim").value + gateway_transaction_cancel_interval = copy_trade_config_map.get("gateway_transaction_cancel_interval").value + rate_oracle_enabled = copy_trade_config_map.get("rate_oracle_enabled").value + quote_conversion_rate = copy_trade_config_map.get("quote_conversion_rate").value + self._initialize_markets([(connector_1, [market_1])]) + base_1, quote_1 = market_1.split("-") + + market_info_1 = MarketTradingPairTuple(self.markets[connector_1], market_1, base_1, quote_1) + self.market_trading_pair_tuples = [market_info_1] + + if rate_oracle_enabled: + rate_source = RateOracle.get_instance() + else: + rate_source = FixedRateSource() + rate_source.add_rate(f"{quote_1}-{quote_1}", Decimal(str(quote_conversion_rate))) # reverse rate is already handled in FixedRateSource find_rate method. + self.strategy = CopyTradeStrategy() + self.strategy.init_params(market_info_1=market_info_1, + wallet_to_copy=wallet_to_copy, + type_of_copy=type_of_copy, + percentage=percentage, + fixed_amount=fixed_amount, + market_1_slippage_buffer=market_1_slippage_buffer, + gateway_transaction_cancel_interval=gateway_transaction_cancel_interval, + rate_source=rate_source, + ) diff --git a/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml b/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml index e8e676baf9f..202609a596f 100644 --- a/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_boost_volume_strategy_TEMPLATE.yml @@ -11,7 +11,17 @@ strategy: null connector_1: null market_1: null -order_amount: null +# The minimum amount of base asset to trade +order_amount_from: null + +# The maximum amount of base asset to trade +order_amount_to: null + +# The minimum delay between placing orders +delay_from: null + +# The delay range between placing orders +delay_range: null # A buffer for which to adjust order price for higher chance of the order getting filled. # This is important for AMM which transaction takes a long time where a slippage is acceptable rather having diff --git a/hummingbot/templates/conf_copy_trade_strategy_TEMPLATE.yml b/hummingbot/templates/conf_copy_trade_strategy_TEMPLATE.yml new file mode 100644 index 00000000000..ab5a9f55154 --- /dev/null +++ b/hummingbot/templates/conf_copy_trade_strategy_TEMPLATE.yml @@ -0,0 +1,40 @@ +########################################## +### Increase Trading Volume strategy config ### +########################################## + +template_version: 6 +strategy: null + +# The following configurations are only required for the Increase Trading Volume trading strategy + +# Connectors and markets parameters +connector_1: null +market_1: null + +# The address of the wallet to copy trades +wallet_to_copy: null + +# The type of copy trade to perform. Options are "percentage" or "fixed_amount" +type_of_copy: null + +percentage: null +fixed_amount: null + +# A buffer for which to adjust order price for higher chance of the order getting filled. +# This is important for AMM which transaction takes a long time where a slippage is acceptable rather having +# the transaction get rejected. The submitted order price will be adjust higher (by percentage value) for buy order +# and lower for sell order. (Enter 1 for 1%) +market_1_slippage_buffer: null + +# A flag (true/false), if true would enable a price shim feature on the AMM connector to allow developer to simulate +# different prices on the AMM market. This is used for development and testing purpose only. +debug_price_shim: false + +# After how many seconds should blockchain transactions be cancelled if they are not included in a block? +gateway_transaction_cancel_interval: 600 + +# What rate source should be used for quote assets pair - between fixed_rate_source and rate_oracle_source? +rate_oracle_enabled: false + +# What is the fixed_rate used to convert quote assets? +quote_conversion_rate: 1 \ No newline at end of file