diff --git a/.coveragerc b/.coveragerc index 6c8a0835de..2ad4051392 100644 --- a/.coveragerc +++ b/.coveragerc @@ -33,6 +33,7 @@ omit = hummingbot/strategy/*/start.py hummingbot/strategy/dev* hummingbot/user/user_balances.py + hummingbot/smart_components/controllers/* dynamic_context = test_function branch = true diff --git a/hummingbot/connector/connector_status.py b/hummingbot/connector/connector_status.py index 3ec5c329f3..0e6ebd5279 100644 --- a/hummingbot/connector/connector_status.py +++ b/hummingbot/connector/connector_status.py @@ -66,6 +66,7 @@ 'plenty': 'bronze', 'woo_x': 'bronze', 'woo_x_testnet': 'bronze', + 'kujira': 'bronze', } warning_messages = { diff --git a/hummingbot/connector/derivative/dydx_perpetual/dydx_perpetual_derivative.py b/hummingbot/connector/derivative/dydx_perpetual/dydx_perpetual_derivative.py index b75c9581c1..882e39d0c0 100644 --- a/hummingbot/connector/derivative/dydx_perpetual/dydx_perpetual_derivative.py +++ b/hummingbot/connector/derivative/dydx_perpetual/dydx_perpetual_derivative.py @@ -256,7 +256,7 @@ async def _place_order( False, amount ).result_price - + price = self.quantize_order_price(trading_pair, price) notional_amount = amount * price if notional_amount not in self._order_notional_amounts.keys(): self._order_notional_amounts[notional_amount] = len(self._order_notional_amounts.keys()) diff --git a/hummingbot/connector/derivative/injective_v2_perpetual/injective_v2_perpetual_derivative.py b/hummingbot/connector/derivative/injective_v2_perpetual/injective_v2_perpetual_derivative.py index 00f3798738..74f2bdf049 100644 --- a/hummingbot/connector/derivative/injective_v2_perpetual/injective_v2_perpetual_derivative.py +++ b/hummingbot/connector/derivative/injective_v2_perpetual/injective_v2_perpetual_derivative.py @@ -378,16 +378,17 @@ async def _create_order( :param position_action: is the order opening or closing a position """ try: - if price is None: + if price is None or price.is_nan(): calculated_price = self.get_price_for_volume( trading_pair=trading_pair, is_buy=trade_type == TradeType.BUY, volume=amount, ).result_price - calculated_price = self.quantize_order_price(trading_pair, calculated_price) else: calculated_price = price + calculated_price = self.quantize_order_price(trading_pair, calculated_price) + await super()._create_order( trade_type=trade_type, order_id=order_id, diff --git a/hummingbot/connector/exchange/injective_v2/account_delegation_script.py b/hummingbot/connector/exchange/injective_v2/account_delegation_script.py index a1922d6fb7..e56e6ac47d 100644 --- a/hummingbot/connector/exchange/injective_v2/account_delegation_script.py +++ b/hummingbot/connector/exchange/injective_v2/account_delegation_script.py @@ -1,8 +1,7 @@ import asyncio from pyinjective.async_client import AsyncClient -from pyinjective.composer import Composer -from pyinjective.constant import Network +from pyinjective.core.network import Network from pyinjective.transaction import Transaction from pyinjective.wallet import PrivateKey @@ -26,10 +25,9 @@ async def main() -> None: - composer = Composer(network=NETWORK.string()) - # initialize grpc client client = AsyncClient(NETWORK, insecure=False) + composer = await client.composer() await client.sync_timeout_height() # load account diff --git a/hummingbot/connector/exchange/injective_v2/data_sources/injective_data_source.py b/hummingbot/connector/exchange/injective_v2/data_sources/injective_data_source.py index 956eebb910..dfa7e76664 100644 --- a/hummingbot/connector/exchange/injective_v2/data_sources/injective_data_source.py +++ b/hummingbot/connector/exchange/injective_v2/data_sources/injective_data_source.py @@ -1,12 +1,10 @@ import asyncio import logging -import os import time from abc import ABC, abstractmethod from decimal import Decimal from enum import Enum from functools import partial -from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union from google.protobuf import any_pb2 @@ -64,11 +62,6 @@ def publisher(self): def query_executor(self): raise NotImplementedError - @property - @abstractmethod - def composer(self) -> Composer: - raise NotImplementedError - @property @abstractmethod def order_creation_lock(self) -> asyncio.Lock: @@ -114,6 +107,10 @@ def portfolio_account_subaccount_index(self) -> int: def network_name(self) -> str: raise NotImplementedError + @abstractmethod + async def composer(self) -> Composer: + raise NotImplementedError + @abstractmethod async def timeout_height(self) -> int: raise NotImplementedError @@ -265,8 +262,6 @@ async def start(self, market_ids: List[str]): async def stop(self): for task in self.events_listening_tasks(): task.cancel() - cookie_file_path = Path(self._chain_cookie_file_path()) - cookie_file_path.unlink(missing_ok=True) def add_listener(self, event_tag: Enum, listener: EventListener): self.publisher.add_listener(event_tag=event_tag, listener=listener) @@ -486,7 +481,7 @@ async def cancel_orders( )) else: market_id = await self.market_id_for_spot_trading_pair(trading_pair=order.trading_pair) - order_data = self._generate_injective_order_data(order=order, market_id=market_id) + order_data = await self._generate_injective_order_data(order=order, market_id=market_id) spot_orders_data.append(order_data) orders_with_hash.append(order) @@ -499,12 +494,12 @@ async def cancel_orders( )) else: market_id = await self.market_id_for_derivative_trading_pair(trading_pair=order.trading_pair) - order_data = self._generate_injective_order_data(order=order, market_id=market_id) + order_data = await self._generate_injective_order_data(order=order, market_id=market_id) derivative_orders_data.append(order_data) orders_with_hash.append(order) if len(orders_with_hash) > 0: - delegated_message = self._order_cancel_message( + delegated_message = await self._order_cancel_message( spot_orders_to_cancel=spot_orders_data, derivative_orders_to_cancel=derivative_orders_data, ) @@ -544,7 +539,7 @@ async def cancel_all_subaccount_orders( spot_markets_ids = spot_markets_ids or [] perpetual_markets_ids = perpetual_markets_ids or [] - delegated_message = self._all_subaccount_orders_cancel_message( + delegated_message = await self._all_subaccount_orders_cancel_message( spot_markets_ids=spot_markets_ids, derivative_markets_ids=perpetual_markets_ids, ) @@ -763,7 +758,7 @@ async def _order_creation_messages( raise NotImplementedError @abstractmethod - def _order_cancel_message( + async def _order_cancel_message( self, spot_orders_to_cancel: List[injective_exchange_tx_pb.OrderData], derivative_orders_to_cancel: List[injective_exchange_tx_pb.OrderData] @@ -771,7 +766,7 @@ def _order_cancel_message( raise NotImplementedError @abstractmethod - def _all_subaccount_orders_cancel_message( + async def _all_subaccount_orders_cancel_message( self, spot_markets_ids: List[str], derivative_markets_ids: List[str] @@ -779,7 +774,7 @@ def _all_subaccount_orders_cancel_message( raise NotImplementedError @abstractmethod - def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: + async def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: raise NotImplementedError @abstractmethod @@ -796,9 +791,6 @@ def _place_order_results( ) -> List[PlaceOrderResult]: raise NotImplementedError - def _chain_cookie_file_path(self) -> str: - return f"{os.path.join(os.path.dirname(__file__), '../.injective_cookie')}" - async def _last_traded_price(self, market_id: str) -> Decimal: price = Decimal("nan") if market_id in await self.spot_market_and_trading_pair_map(): @@ -1041,8 +1033,9 @@ async def _send_in_transaction(self, messages: List[any_pb2.Any]) -> Dict[str, A await self.initialize_trading_account() raise + composer = await self.composer() gas_limit = int(simulation_result["gasInfo"]["gasUsed"]) + CONSTANTS.EXTRA_TRANSACTION_GAS - fee = [self.composer.Coin( + fee = [composer.Coin( amount=gas_limit * CONSTANTS.DEFAULT_GAS_PRICE, denom=self.fee_denom, )] @@ -1299,8 +1292,9 @@ async def _process_transaction_update(self, transaction_event: Dict[str, Any]): self.publisher.trigger_event(event_tag=InjectiveEvent.ChainTransactionEvent, message=transaction_event) async def _create_spot_order_definition(self, order: GatewayInFlightOrder): + composer = await self.composer() market_id = await self.market_id_for_spot_trading_pair(order.trading_pair) - definition = self.composer.SpotOrder( + definition = composer.SpotOrder( market_id=market_id, subaccount_id=self.portfolio_account_subaccount_id, fee_recipient=self.portfolio_account_injective_address, @@ -1312,8 +1306,9 @@ async def _create_spot_order_definition(self, order: GatewayInFlightOrder): return definition async def _create_derivative_order_definition(self, order: GatewayPerpetualInFlightOrder): + composer = await self.composer() market_id = await self.market_id_for_derivative_trading_pair(order.trading_pair) - definition = self.composer.DerivativeOrder( + definition = composer.DerivativeOrder( market_id=market_id, subaccount_id=self.portfolio_account_subaccount_id, fee_recipient=self.portfolio_account_injective_address, diff --git a/hummingbot/connector/exchange/injective_v2/data_sources/injective_grantee_data_source.py b/hummingbot/connector/exchange/injective_v2/data_sources/injective_grantee_data_source.py index e514d7e1dc..d6df605825 100644 --- a/hummingbot/connector/exchange/injective_v2/data_sources/injective_grantee_data_source.py +++ b/hummingbot/connector/exchange/injective_v2/data_sources/injective_grantee_data_source.py @@ -10,7 +10,7 @@ from pyinjective import Transaction from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer, injective_exchange_tx_pb -from pyinjective.constant import Network +from pyinjective.core.network import Network from pyinjective.orderhash import OrderHashManager from pyinjective.wallet import Address, PrivateKey @@ -50,9 +50,8 @@ def __init__( self._client = AsyncClient( network=self._network, insecure=not use_secure_connection, - chain_cookie_location=self._chain_cookie_file_path(), ) - self._composer = Composer(network=self._network.string()) + self._composer = None self._query_executor = PythonSDKInjectiveQueryExecutor(sdk_client=self._client) self._private_key = None @@ -99,10 +98,6 @@ def publisher(self): def query_executor(self): return self._query_executor - @property - def composer(self) -> Composer: - return self._composer - @property def order_creation_lock(self) -> asyncio.Lock: return self._order_creation_lock @@ -139,6 +134,11 @@ def portfolio_account_subaccount_index(self) -> int: def network_name(self) -> str: return self._network.string() + async def composer(self) -> Composer: + if self._composer is None: + self._composer = await self._client.composer() + return self._composer + def events_listening_tasks(self) -> List[asyncio.Task]: return self._events_listening_tasks.copy() @@ -509,7 +509,7 @@ async def _order_creation_messages( spot_orders_to_create: List[GatewayInFlightOrder], derivative_orders_to_create: List[GatewayPerpetualInFlightOrder], ) -> Tuple[List[any_pb2.Any], List[str], List[str]]: - composer = self.composer + composer = await self.composer() spot_market_order_definitions = [] derivative_market_order_definitions = [] spot_order_definitions = [] @@ -580,12 +580,12 @@ async def _order_creation_messages( return [delegated_message], spot_order_hashes, derivative_order_hashes - def _order_cancel_message( + async def _order_cancel_message( self, spot_orders_to_cancel: List[injective_exchange_tx_pb.OrderData], derivative_orders_to_cancel: List[injective_exchange_tx_pb.OrderData] ) -> any_pb2.Any: - composer = self.composer + composer = await self.composer() message = composer.MsgBatchUpdateOrders( sender=self.portfolio_account_injective_address, @@ -598,12 +598,12 @@ def _order_cancel_message( ) return delegated_message - def _all_subaccount_orders_cancel_message( + async def _all_subaccount_orders_cancel_message( self, spot_markets_ids: List[str], derivative_markets_ids: List[str] ) -> any_pb2.Any: - composer = self.composer + composer = await self.composer() message = composer.MsgBatchUpdateOrders( sender=self.portfolio_account_injective_address, @@ -617,8 +617,9 @@ def _all_subaccount_orders_cancel_message( ) return delegated_message - def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: - order_data = self.composer.OrderData( + async def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: + composer = await self.composer() + order_data = composer.OrderData( market_id=market_id, subaccount_id=self.portfolio_account_subaccount_id, order_hash=order.exchange_order_id, diff --git a/hummingbot/connector/exchange/injective_v2/data_sources/injective_read_only_data_source.py b/hummingbot/connector/exchange/injective_v2/data_sources/injective_read_only_data_source.py index 6e23aa9cf7..a1cb8f5575 100644 --- a/hummingbot/connector/exchange/injective_v2/data_sources/injective_read_only_data_source.py +++ b/hummingbot/connector/exchange/injective_v2/data_sources/injective_read_only_data_source.py @@ -6,7 +6,7 @@ from pyinjective import Transaction from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer, injective_exchange_tx_pb -from pyinjective.constant import Network +from pyinjective.core.network import Network from hummingbot.connector.exchange.injective_v2 import injective_constants as CONSTANTS from hummingbot.connector.exchange.injective_v2.data_sources.injective_data_source import InjectiveDataSource @@ -40,9 +40,8 @@ def __init__( self._client = AsyncClient( network=self._network, insecure=not use_secure_connection, - chain_cookie_location=self._chain_cookie_file_path(), ) - self._composer = Composer(network=self._network.string()) + self._composer = None self._query_executor = PythonSDKInjectiveQueryExecutor(sdk_client=self._client) self._publisher = PubSub() @@ -67,10 +66,6 @@ def publisher(self): def query_executor(self): return self._query_executor - @property - def composer(self) -> Composer: - return self._composer - @property def order_creation_lock(self) -> asyncio.Lock: return None @@ -107,6 +102,11 @@ def portfolio_account_subaccount_index(self) -> int: def network_name(self) -> str: return self._network.string() + async def composer(self) -> Composer: + if self._composer is None: + self._composer = await self._client.composer() + return self._composer + async def timeout_height(self) -> int: raise NotImplementedError @@ -337,22 +337,25 @@ async def _order_creation_messages( ) -> Tuple[List[any_pb2.Any], List[str], List[str]]: raise NotImplementedError - def _order_cancel_message( + async def _order_cancel_message( self, spot_orders_to_cancel: List[injective_exchange_tx_pb.OrderData], derivative_orders_to_cancel: List[injective_exchange_tx_pb.OrderData] ) -> any_pb2.Any: raise NotImplementedError - def _all_subaccount_orders_cancel_message( + async def _all_subaccount_orders_cancel_message( self, spot_orders_to_cancel: List[injective_exchange_tx_pb.OrderData], derivative_orders_to_cancel: List[injective_exchange_tx_pb.OrderData] ) -> any_pb2.Any: raise NotImplementedError - def _generate_injective_order_data(self, order: GatewayInFlightOrder, - market_id: str) -> injective_exchange_tx_pb.OrderData: + async def _generate_injective_order_data( + self, + order: GatewayInFlightOrder, + market_id: str, + ) -> injective_exchange_tx_pb.OrderData: raise NotImplementedError async def _updated_derivative_market_info_for_id(self, market_id: str) -> InjectiveDerivativeMarket: diff --git a/hummingbot/connector/exchange/injective_v2/data_sources/injective_vaults_data_source.py b/hummingbot/connector/exchange/injective_v2/data_sources/injective_vaults_data_source.py index d7ad51e264..f427694acc 100644 --- a/hummingbot/connector/exchange/injective_v2/data_sources/injective_vaults_data_source.py +++ b/hummingbot/connector/exchange/injective_v2/data_sources/injective_vaults_data_source.py @@ -10,7 +10,7 @@ from pyinjective import Transaction from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer, injective_exchange_tx_pb -from pyinjective.constant import Network +from pyinjective.core.network import Network from pyinjective.orderhash import OrderHashManager from pyinjective.wallet import Address, PrivateKey @@ -50,9 +50,8 @@ def __init__( self._client = AsyncClient( network=self._network, insecure=not use_secure_connection, - chain_cookie_location=self._chain_cookie_file_path(), ) - self._composer = Composer(network=self._network.string()) + self._composer = None self._query_executor = PythonSDKInjectiveQueryExecutor(sdk_client=self._client) self._private_key = None @@ -99,10 +98,6 @@ def publisher(self): def query_executor(self): return self._query_executor - @property - def composer(self) -> Composer: - return self._composer - @property def order_creation_lock(self) -> asyncio.Lock: return self._order_creation_lock @@ -139,6 +134,11 @@ def portfolio_account_subaccount_index(self) -> int: def network_name(self) -> str: return self._network.string() + async def composer(self) -> Composer: + if self._composer is None: + self._composer = await self._client.composer() + return self._composer + def events_listening_tasks(self) -> List[asyncio.Task]: return self._events_listening_tasks.copy() @@ -461,7 +461,7 @@ async def _order_creation_messages( spot_orders_to_create: List[GatewayInFlightOrder], derivative_orders_to_create: List[GatewayPerpetualInFlightOrder], ) -> Tuple[List[any_pb2.Any], List[str], List[str]]: - composer = self.composer + composer = await self.composer() spot_order_definitions = [] derivative_order_definitions = [] @@ -497,12 +497,12 @@ async def _order_creation_messages( return [execute_contract_message], [], [] - def _order_cancel_message( + async def _order_cancel_message( self, spot_orders_to_cancel: List[injective_exchange_tx_pb.OrderData], derivative_orders_to_cancel: List[injective_exchange_tx_pb.OrderData] ) -> any_pb2.Any: - composer = self.composer + composer = await self.composer() message = composer.MsgBatchUpdateOrders( sender=self.portfolio_account_injective_address, @@ -528,12 +528,12 @@ def _order_cancel_message( return execute_contract_message - def _all_subaccount_orders_cancel_message( + async def _all_subaccount_orders_cancel_message( self, spot_markets_ids: List[str], derivative_markets_ids: List[str] ) -> any_pb2.Any: - composer = self.composer + composer = await self.composer() message = composer.MsgBatchUpdateOrders( sender=self.portfolio_account_injective_address, @@ -560,8 +560,9 @@ def _all_subaccount_orders_cancel_message( return execute_contract_message - def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: - order_data = self.composer.OrderData( + async def _generate_injective_order_data(self, order: GatewayInFlightOrder, market_id: str) -> injective_exchange_tx_pb.OrderData: + composer = await self.composer() + order_data = composer.OrderData( market_id=market_id, subaccount_id=str(self.portfolio_account_subaccount_index), order_hash=order.exchange_order_id, @@ -575,7 +576,8 @@ async def _create_spot_order_definition(self, order: GatewayInFlightOrder): # Both price and quantity have to be adjusted because the vaults expect to receive those values without # the extra 18 zeros that the chain backend expects for direct trading messages market_id = await self.market_id_for_spot_trading_pair(order.trading_pair) - definition = self.composer.SpotOrder( + composer = await self.composer() + definition = composer.SpotOrder( market_id=market_id, subaccount_id=str(self.portfolio_account_subaccount_index), fee_recipient=self.portfolio_account_injective_address, @@ -593,7 +595,8 @@ async def _create_derivative_order_definition(self, order: GatewayPerpetualInFli # Price, quantity and margin have to be adjusted because the vaults expect to receive those values without # the extra 18 zeros that the chain backend expects for direct trading messages market_id = await self.market_id_for_derivative_trading_pair(order.trading_pair) - definition = self.composer.DerivativeOrder( + composer = await self.composer() + definition = composer.DerivativeOrder( market_id=market_id, subaccount_id=str(self.portfolio_account_subaccount_index), fee_recipient=self.portfolio_account_injective_address, diff --git a/hummingbot/connector/exchange/injective_v2/injective_v2_exchange.py b/hummingbot/connector/exchange/injective_v2/injective_v2_exchange.py index 125fe4caa8..f03d075dd9 100644 --- a/hummingbot/connector/exchange/injective_v2/injective_v2_exchange.py +++ b/hummingbot/connector/exchange/injective_v2/injective_v2_exchange.py @@ -323,16 +323,17 @@ async def _create_order(self, :param price: the order price """ try: - if price is None: + if price is None or price.is_nan(): calculated_price = self.get_price_for_volume( trading_pair=trading_pair, is_buy=trade_type == TradeType.BUY, volume=amount, ).result_price - calculated_price = self.quantize_order_price(trading_pair, calculated_price) else: calculated_price = price + calculated_price = self.quantize_order_price(trading_pair, calculated_price) + await super()._create_order( trade_type=trade_type, order_id=order_id, diff --git a/hummingbot/connector/exchange/injective_v2/injective_v2_utils.py b/hummingbot/connector/exchange/injective_v2/injective_v2_utils.py index 4636bdfa8d..9fe48d11d2 100644 --- a/hummingbot/connector/exchange/injective_v2/injective_v2_utils.py +++ b/hummingbot/connector/exchange/injective_v2/injective_v2_utils.py @@ -4,7 +4,7 @@ from pydantic import Field, SecretStr from pydantic.class_validators import validator -from pyinjective.constant import Network +from pyinjective.core.network import Network from hummingbot.client.config.config_data_types import BaseClientModel, BaseConnectorConfigMap, ClientFieldData from hummingbot.connector.exchange.injective_v2 import injective_constants as CONSTANTS @@ -31,7 +31,6 @@ taker_percent_fee_decimal=Decimal("0"), ) -MAINNET_NODES = ["lb", "sentry0", "sentry1", "sentry3"] TESTNET_NODES = ["lb", "sentry"] @@ -47,28 +46,14 @@ def use_secure_connection(self) -> bool: class InjectiveMainnetNetworkMode(InjectiveNetworkMode): - node: str = Field( - default="lb", - client_data=ClientFieldData( - prompt=lambda cm: (f"Enter the mainnet node you want to connect to ({'/'.join(MAINNET_NODES)})"), - prompt_on_new=True - ), - ) - class Config: title = "mainnet_network" - @validator("node", pre=True) - def validate_node(cls, v: str): - if v not in MAINNET_NODES: - raise ValueError(f"{v} is not a valid node ({MAINNET_NODES})") - return v - def network(self) -> Network: - return Network.mainnet(node=self.node) + return Network.mainnet() def use_secure_connection(self) -> bool: - return self.node == "lb" + return True def rate_limits(self) -> List[RateLimit]: return CONSTANTS.PUBLIC_NODE_RATE_LIMITS diff --git a/hummingbot/connector/exchange/polkadex/polkadex_api_order_book_data_source.py b/hummingbot/connector/exchange/polkadex/polkadex_api_order_book_data_source.py index 0d608fab7f..eb003cf180 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_api_order_book_data_source.py @@ -1,13 +1,12 @@ import asyncio -from copy import copy, deepcopy from typing import TYPE_CHECKING, Any, Dict, List, Optional from hummingbot.connector.exchange.polkadex import polkadex_constants as CONSTANTS from hummingbot.connector.exchange.polkadex.polkadex_data_source import PolkadexDataSource -from hummingbot.connector.exchange.polkadex.polkadex_events import PolkadexOrderBookEvent -from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType +from hummingbot.core.data_type.order_book_message import OrderBookMessage from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource from hummingbot.core.event.event_forwarder import EventForwarder +from hummingbot.core.event.events import OrderBookEvent from hummingbot.core.web_assistant.ws_assistant import WSAssistant if TYPE_CHECKING: @@ -67,43 +66,23 @@ async def _order_book_snapshot(self, trading_pair: str) -> OrderBookMessage: async def _parse_trade_message(self, raw_message: OrderBookMessage, message_queue: asyncio.Queue): # In Polkadex 'raw_message' is not a raw message, but the OrderBookMessage with type Trade created # by the data source - - message_content = deepcopy(raw_message.content) - message_content["trading_pair"] = await self._connector.trading_pair_associated_to_exchange_symbol( - symbol=message_content["trading_pair"] - ) - - trade_message = OrderBookMessage( - message_type=OrderBookMessageType.TRADE, - content=message_content, - timestamp=raw_message.timestamp, - ) - message_queue.put_nowait(trade_message) + message_queue.put_nowait(raw_message) async def _parse_order_book_diff_message(self, raw_message: OrderBookMessage, message_queue: asyncio.Queue): # In Polkadex 'raw_message' is not a raw message, but the OrderBookMessage with type Trade created # by the data source - message_content = copy(raw_message.content) - message_content["trading_pair"] = await self._connector.trading_pair_associated_to_exchange_symbol( - symbol=message_content["trading_pair"] - ) - diff_message = OrderBookMessage( - message_type=OrderBookMessageType.DIFF, - content=message_content, - timestamp=raw_message.timestamp, - ) - message_queue.put_nowait(diff_message) + message_queue.put_nowait(raw_message) def _configure_event_forwarders(self): event_forwarder = EventForwarder(to_function=self._process_order_book_event) self._forwarders.append(event_forwarder) self._data_source.add_listener( - event_tag=PolkadexOrderBookEvent.OrderBookDataSourceUpdateEvent, listener=event_forwarder + event_tag=OrderBookEvent.OrderBookDataSourceUpdateEvent, listener=event_forwarder ) event_forwarder = EventForwarder(to_function=self._process_public_trade_event) self._forwarders.append(event_forwarder) - self._data_source.add_listener(event_tag=PolkadexOrderBookEvent.PublicTradeEvent, listener=event_forwarder) + self._data_source.add_listener(event_tag=OrderBookEvent.TradeEvent, listener=event_forwarder) def _process_order_book_event(self, order_book_diff: OrderBookMessage): self._message_queue[self._diff_messages_queue_key].put_nowait(order_book_diff) diff --git a/hummingbot/connector/exchange/polkadex/polkadex_constants.py b/hummingbot/connector/exchange/polkadex/polkadex_constants.py index 036013d8f8..6f65c8f5e8 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_constants.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_constants.py @@ -10,15 +10,12 @@ CLIENT_ID_PREFIX = "HBOT" DEFAULT_DOMAIN = "" -TESTNET_DOMAIN = "testnet" GRAPHQL_ENDPOINTS = { - DEFAULT_DOMAIN: "https://gu5xqmhhcnfeveotzwhe6ohfba.appsync-api.eu-central-1.amazonaws.com/graphql", - TESTNET_DOMAIN: "https://kckpespz5bb2rmdnuxycz6e7he.appsync-api.eu-central-1.amazonaws.com/graphql", + DEFAULT_DOMAIN: "https://yx375ldozvcvthjk2nczch3fhq.appsync-api.eu-central-1.amazonaws.com/graphql", } BLOCKCHAIN_URLS = { - DEFAULT_DOMAIN: "wss://mainnet.polkadex.trade", - TESTNET_DOMAIN: "wss://blockchain.polkadex.trade", + DEFAULT_DOMAIN: "wss://polkadex.public.curie.radiumblock.co/ws", } POLKADEX_SS58_PREFIX = 88 @@ -32,10 +29,12 @@ FIND_USER_LIMIT_ID = "FindUser" PUBLIC_TRADES_LIMIT_ID = "RecentTrades" ALL_BALANCES_LIMIT_ID = "AllBalances" +ALL_FILLS_LIMIT_ID = "AllFills" PLACE_ORDER_LIMIT_ID = "PlaceOrder" CANCEL_ORDER_LIMIT_ID = "CancelOrder" BATCH_ORDER_UPDATES_LIMIT_ID = "BatchOrderUpdates" ORDER_UPDATE_LIMIT_ID = "OrderUpdate" +LIST_OPEN_ORDERS_LIMIT_ID = "ListOpenOrders" NO_LIMIT = sys.maxsize @@ -70,6 +69,11 @@ limit=NO_LIMIT, time_interval=SECOND, ), + RateLimit( + limit_id=ALL_FILLS_LIMIT_ID, + limit=NO_LIMIT, + time_interval=SECOND, + ), RateLimit( limit_id=PLACE_ORDER_LIMIT_ID, limit=NO_LIMIT, @@ -90,6 +94,11 @@ limit=NO_LIMIT, time_interval=SECOND, ), + RateLimit( + limit_id=LIST_OPEN_ORDERS_LIMIT_ID, + limit=NO_LIMIT, + time_interval=SECOND, + ), ] @@ -119,14 +128,7 @@ ["timestamp", "i64"], ], }, - "CancelOrderPayload": {"type": "struct", "type_mapping": [["id", "String"]]}, - "TradingPair": { - "type": "struct", - "type_mapping": [ - ["base_asset", "AssetId"], - ["quote_asset", "AssetId"], - ], - }, + "order_id": "H256", "OrderSide": { "type": "enum", "type_mapping": [ @@ -134,13 +136,6 @@ ["Bid", "Null"], ], }, - "AssetId": { - "type": "enum", - "type_mapping": [ - ["asset", "u128"], - ["polkadex", "Null"], - ], - }, "OrderType": { "type": "enum", "type_mapping": [ diff --git a/hummingbot/connector/exchange/polkadex/polkadex_data_source.py b/hummingbot/connector/exchange/polkadex/polkadex_data_source.py index 2119815f4c..ffbc87c8f8 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_data_source.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_data_source.py @@ -3,31 +3,34 @@ import logging import time from decimal import Decimal -from typing import Any, Dict, List, Mapping, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple from urllib.parse import urlparse from bidict import bidict from gql.transport.appsync_auth import AppSyncJWTAuthentication +from scalecodec import ScaleBytes from substrateinterface import Keypair, KeypairType, SubstrateInterface from hummingbot.connector.exchange.polkadex import polkadex_constants as CONSTANTS, polkadex_utils -from hummingbot.connector.exchange.polkadex.polkadex_events import PolkadexOrderBookEvent from hummingbot.connector.exchange.polkadex.polkadex_query_executor import GrapQLQueryExecutor from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.connector.utils import combine_to_hb_trading_pair, split_hb_trading_pair from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.api_throttler.async_throttler_base import AsyncThrottlerBase from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, OrderUpdate, TradeUpdate from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType -from hummingbot.core.data_type.trade_fee import TokenAmount, TradeFeeBase, TradeFeeSchema +from hummingbot.core.data_type.trade_fee import TokenAmount, TradeFeeBase from hummingbot.core.event.event_listener import EventListener -from hummingbot.core.event.events import AccountEvent, BalanceUpdateEvent, MarketEvent +from hummingbot.core.event.events import AccountEvent, BalanceUpdateEvent, MarketEvent, OrderBookEvent from hummingbot.core.network_iterator import NetworkStatus from hummingbot.core.pubsub import Enum, PubSub from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.logger import HummingbotLogger +if TYPE_CHECKING: + from hummingbot.connector.exchange_py_base import ExchangePyBase + class PolkadexDataSource: _logger: Optional[HummingbotLogger] = None @@ -38,8 +41,16 @@ def logger(cls) -> HummingbotLogger: cls._logger = logging.getLogger(HummingbotLogger.logger_name_for_class(cls)) return cls._logger - def __init__(self, seed_phrase: str, domain: Optional[str] = CONSTANTS.DEFAULT_DOMAIN): + def __init__( + self, + connector: "ExchangePyBase", + seed_phrase: str, + domain: Optional[str] = CONSTANTS.DEFAULT_DOMAIN, + trading_required: bool = True, + ): + self._connector = connector self._domain = domain + self._trading_required = trading_required graphql_host = CONSTANTS.GRAPHQL_ENDPOINTS[self._domain] netloc_host = urlparse(graphql_host).netloc self._keypair = None @@ -51,15 +62,10 @@ def __init__(self, seed_phrase: str, domain: Optional[str] = CONSTANTS.DEFAULT_D self._user_proxy_address = self._keypair.ss58_address self._auth = AppSyncJWTAuthentication(netloc_host, self._user_proxy_address) else: - self._user_proxy_address = "no_address" - self._auth = AppSyncJWTAuthentication(netloc_host, "no_address") + self._user_proxy_address = "READ_ONLY" + self._auth = AppSyncJWTAuthentication(netloc_host, "READ_ONLY") - self._substrate_interface = SubstrateInterface( - url=CONSTANTS.BLOCKCHAIN_URLS[self._domain], - ss58_format=CONSTANTS.POLKADEX_SS58_PREFIX, - type_registry=CONSTANTS.CUSTOM_TYPES, - auto_discover=False, - ) + self._substrate_interface = self._build_substrate_interface() self._query_executor = GrapQLQueryExecutor(auth=self._auth, domain=self._domain) self._publisher = PubSub() @@ -68,17 +74,25 @@ def __init__(self, seed_phrase: str, domain: Optional[str] = CONSTANTS.DEFAULT_D # The connector using this data source should replace the throttler with the one used by the connector. self._throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self._events_listening_tasks = [] - self._assets_map: Optional[Dict[str, str]] = None + self._assets_map: Dict[str, str] = {} self._polkadex_order_type = { OrderType.MARKET: "MARKET", OrderType.LIMIT: "LIMIT", OrderType.LIMIT_MAKER: "LIMIT", } + self._hummingbot_order_type = { + "LIMIT": OrderType.LIMIT, + "MARKET": OrderType.MARKET, + } self._polkadex_trade_type = { TradeType.BUY: "Bid", TradeType.SELL: "Ask", } + self._hummingbot_trade_type = { + "Bid": TradeType.BUY, + "Ask": TradeType.SELL, + } def is_started(self) -> bool: return len(self._events_listening_tasks) > 0 @@ -87,8 +101,6 @@ async def start(self, market_symbols: List[str]): if len(self._events_listening_tasks) > 0: raise AssertionError("Polkadex datasource is already listening to events and can't be started again") - main_address = await self.user_main_address() - for market_symbol in market_symbols: self._events_listening_tasks.append( asyncio.create_task( @@ -105,20 +117,22 @@ async def start(self, market_symbols: List[str]): ) ) - self._events_listening_tasks.append( - asyncio.create_task( - self._query_executor.listen_to_private_events( - events_handler=self._process_private_event, address=self._user_proxy_address + if self._trading_required: + self._events_listening_tasks.append( + asyncio.create_task( + self._query_executor.listen_to_private_events( + events_handler=self._process_private_event, address=self._user_proxy_address + ) ) ) - ) - self._events_listening_tasks.append( - asyncio.create_task( - self._query_executor.listen_to_private_events( - events_handler=self._process_private_event, address=main_address + main_address = await self.user_main_address() + self._events_listening_tasks.append( + asyncio.create_task( + self._query_executor.listen_to_private_events( + events_handler=self._process_private_event, address=main_address + ) ) ) - ) async def stop(self): for task in self._events_listening_tasks: @@ -148,20 +162,17 @@ async def exchange_status(self): return result async def assets_map(self) -> Dict[str, str]: - if self._assets_map is None: - async with self._throttler.execute_task(limit_id=CONSTANTS.ALL_ASSETS_LIMIT_ID): - all_assets = await self._query_executor.all_assets() - self._assets_map = { - asset["asset_id"]: polkadex_utils.normalized_asset_name( - asset_id=asset["asset_id"], asset_name=asset["name"] - ) - for asset in all_assets["getAllAssets"]["items"] - } + async with self._throttler.execute_task(limit_id=CONSTANTS.ALL_ASSETS_LIMIT_ID): + all_assets = await self._query_executor.all_assets() + self._assets_map = { + asset["asset_id"]: polkadex_utils.normalized_asset_name( + asset_id=asset["asset_id"], asset_name=asset["name"] + ) + for asset in all_assets["getAllAssets"]["items"] + } - if len(self._assets_map) > 0: - self._assets_map[ - "polkadex" - ] = "PDEX" # required due to inconsistent token name in private balance event + if len(self._assets_map) > 0: + self._assets_map["polkadex"] = "PDEX" # required due to inconsistent token name in private balance event return self._assets_map @@ -189,7 +200,10 @@ async def all_trading_rules(self) -> List[TradingRule]: trading_rules = [] for market_info in markets["getAllMarkets"]["items"]: try: - trading_pair = market_info["market"] + exchange_trading_pair = market_info["market"] + trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol( + symbol=exchange_trading_pair + ) min_order_size = Decimal(market_info["min_order_qty"]) max_order_size = Decimal(market_info["max_order_qty"]) min_order_price = Decimal(market_info["min_order_price"]) @@ -234,6 +248,8 @@ async def order_book_snapshot(self, market_symbol: str, trading_pair: str) -> Or else: asks.append((price, amount)) + update_id = max(update_id, int(orderbook_entry["stid"])) + order_book_message_content = { "trading_pair": trading_pair, "update_id": update_id, @@ -293,19 +309,18 @@ async def place_order( order_type: OrderType, ) -> Tuple[str, float]: main_account = await self.user_main_address() - translated_client_order_id = f"0x{client_order_id.encode('utf-8').hex()}" - price = round(price, 4) - amount = round(amount, 4) + price = self.normalize_fraction(price) + amount = self.normalize_fraction(amount) timestamp = self._time() order_parameters = { "user": self._user_proxy_address, "main_account": main_account, "pair": market_symbol, - "qty": f"{amount:f}"[:12], - "price": f"{price:f}"[:12], - "quote_order_quantity": "0", - "timestamp": int(timestamp), - "client_order_id": translated_client_order_id, + "qty": f"{amount}", + "price": f"{price}", + "quote_order_quantity": "0", # No need to be 8 decimal points + "timestamp": int(timestamp * 1e3), + "client_order_id": client_order_id, "order_type": self._polkadex_order_type[order_type], "side": self._polkadex_trade_type[trade_type], } @@ -318,57 +333,29 @@ async def place_order( polkadex_order=order_parameters, signature={"Sr25519": signature.hex()}, ) + place_order_data = json.loads(response["place_order"]) - exchange_order_id = response["place_order"] + exchange_order_id = None + if place_order_data["is_success"] is True: + exchange_order_id = place_order_data["body"] if exchange_order_id is None: raise ValueError(f"Error in Polkadex creating order {client_order_id}") return exchange_order_id, timestamp - async def cancel_order(self, order: InFlightOrder, market_symbol: str, timestamp: float) -> bool: - cancel_request = self._substrate_interface.create_scale_object("H256").encode(order.exchange_order_id) - signature = self._keypair.sign(cancel_request) - - async with self._throttler.execute_task(limit_id=CONSTANTS.CANCEL_ORDER_LIMIT_ID): - cancel_result = await self._query_executor.cancel_order( - order_id=order.exchange_order_id, - market_symbol=market_symbol, - proxy_address=self._user_proxy_address, - signature={"Sr25519": signature.hex()}, - ) - - if cancel_result["cancel_order"]: - success = True + async def cancel_order(self, order: InFlightOrder, market_symbol: str, timestamp: float) -> OrderState: + try: + cancel_result = await self._place_order_cancel(order=order, market_symbol=market_symbol) + except Exception as e: + if "Order is not active" in str(e): + new_order_state = OrderState.CANCELED + else: + raise else: - success = False - - return success - - async def order_updates_from_account(self, from_time: float) -> List[OrderUpdate]: - order_updates = [] - async with self._throttler.execute_task(limit_id=CONSTANTS.BATCH_ORDER_UPDATES_LIMIT_ID): - response = await self._query_executor.list_order_history_by_account( - main_account=self._user_proxy_address, - from_time=from_time, - to_time=self._time(), - ) + new_order_state = OrderState.PENDING_CANCEL if cancel_result["cancel_order"] else order.current_state - for order_info in response["listOrderHistorybyMainAccount"]["items"]: - new_state = CONSTANTS.ORDER_STATE[order_info["st"]] - filled_amount = Decimal(order_info["fq"]) - if new_state == OrderState.OPEN and filled_amount > 0: - new_state = OrderState.PARTIALLY_FILLED - order_update = OrderUpdate( - client_order_id=order_info["cid"], - exchange_order_id=order_info["id"], - trading_pair=order_info["m"], - update_timestamp=self._time(), - new_state=new_state, - ) - order_updates.append(order_update) - - return order_updates + return new_order_state async def order_update(self, order: InFlightOrder, market_symbol: str) -> OrderUpdate: async with self._throttler.execute_task(limit_id=CONSTANTS.ORDER_UPDATE_LIMIT_ID): @@ -396,23 +383,102 @@ async def order_update(self, order: InFlightOrder, market_symbol: str) -> OrderU ) return order_update + async def get_all_fills( + self, from_timestamp: float, to_timestamp: float, orders: List[InFlightOrder] + ) -> List[TradeUpdate]: + trade_updates = [] + + async with self._throttler.execute_task(limit_id=CONSTANTS.ALL_FILLS_LIMIT_ID): + fills = await self._query_executor.get_order_fills_by_main_account( + from_timestamp=from_timestamp, to_timestamp=to_timestamp, main_account=self._user_proxy_address + ) + + exchange_order_id_to_order = {order.exchange_order_id: order for order in orders} + for fill in fills["listTradesByMainAccount"]["items"]: + exchange_trading_pair = fill["m"] + trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol( + symbol=exchange_trading_pair + ) + + price = Decimal(fill["p"]) + size = Decimal(fill["q"]) + order = exchange_order_id_to_order.get(fill["m_id"], None) + if order is None: + order = exchange_order_id_to_order.get(fill["t_id"], None) + if order is not None: + exchange_order_id = order.exchange_order_id + client_order_id = order.client_order_id + + fee = await self._build_fee_for_event(event=fill, trade_type=order.trade_type) + trade_updates.append( + TradeUpdate( + trade_id=fill["trade_id"], + client_order_id=client_order_id, + exchange_order_id=exchange_order_id, + trading_pair=trading_pair, + fill_timestamp=int(fill["t"]) * 1e-3, + fill_price=price, + fill_base_amount=size, + fill_quote_amount=price * size, + fee=fee, + ) + ) + + return trade_updates + + async def _place_order_cancel(self, order: InFlightOrder, market_symbol: str) -> Dict[str, Any]: + cancel_request = self._build_substrate_request_with_retries( + type_string="H256", encode_value=order.exchange_order_id + ) + signature = self._keypair.sign(cancel_request) + + async with self._throttler.execute_task(limit_id=CONSTANTS.CANCEL_ORDER_LIMIT_ID): + cancel_result = await self._query_executor.cancel_order( + order_id=order.exchange_order_id, + market_symbol=market_symbol, + main_address=self._user_main_address, + proxy_address=self._user_proxy_address, + signature={"Sr25519": signature.hex()}, + ) + + return cancel_result + + def _build_substrate_request_with_retries( + self, type_string: str, encode_value: Any, retries_left: int = 1 + ) -> ScaleBytes: + try: + request = self._substrate_interface.create_scale_object(type_string=type_string).encode(value=encode_value) + except BrokenPipeError: + self.logger().exception("Rebuilding the substrate interface.") + if retries_left == 0: + raise + self._substrate_interface = self._build_substrate_interface() + request = self._build_substrate_request_with_retries( + type_string=type_string, encode_value=encode_value, retries_left=retries_left - 1 + ) + return request + + def _build_substrate_interface(self) -> SubstrateInterface: + substrate_interface = SubstrateInterface( + url=CONSTANTS.BLOCKCHAIN_URLS[self._domain], + ss58_format=CONSTANTS.POLKADEX_SS58_PREFIX, + type_registry=CONSTANTS.CUSTOM_TYPES, + auto_discover=False, + ) + return substrate_interface + def _process_order_book_event(self, event: Dict[str, Any], market_symbol: str): + safe_ensure_future(self._process_order_book_event_async(event=event, market_symbol=market_symbol)) + + async def _process_order_book_event_async(self, event: Dict[str, Any], market_symbol: str): diff_data = json.loads(event["websocket_streams"]["data"]) timestamp = self._time() - update_id = -1 - bids = [] - asks = [] - - for diff_update in diff_data["changes"]: - update_id = max(update_id, diff_update[3]) - price_amount_pair = (diff_update[1], diff_update[2]) - if diff_update[0] == "Bid": - bids.append(price_amount_pair) - else: - asks.append(price_amount_pair) + update_id = diff_data["i"] + asks = [(Decimal(price), Decimal(amount)) for price, amount in diff_data["a"].items()] + bids = [(Decimal(price), Decimal(amount)) for price, amount in diff_data["b"].items()] order_book_message_content = { - "trading_pair": market_symbol, + "trading_pair": await self._connector.trading_pair_associated_to_exchange_symbol(symbol=market_symbol), "update_id": update_id, "bids": bids, "asks": asks, @@ -422,19 +488,21 @@ def _process_order_book_event(self, event: Dict[str, Any], market_symbol: str): content=order_book_message_content, timestamp=timestamp, ) - self._publisher.trigger_event( - event_tag=PolkadexOrderBookEvent.OrderBookDataSourceUpdateEvent, message=diff_message - ) + self._publisher.trigger_event(event_tag=OrderBookEvent.OrderBookDataSourceUpdateEvent, message=diff_message) def _process_recent_trades_event(self, event: Dict[str, Any]): + safe_ensure_future(self._process_recent_trades_event_async(event=event)) + + async def _process_recent_trades_event_async(self, event: Dict[str, Any]): trade_data = json.loads(event["websocket_streams"]["data"]) - symbol = trade_data["m"] + exchange_trading_pair = trade_data["m"] + trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol=exchange_trading_pair) timestamp = int(trade_data["t"]) * 1e-3 - trade_type = float(TradeType.SELL.value) # Unfortunately Polkadex does not indicate the trade side + trade_type = float(self._hummingbot_trade_type[trade_data["m_side"]].value) message_content = { - "trade_id": trade_data["tid"], - "trading_pair": symbol, + "trade_id": trade_data["trade_id"], + "trading_pair": trading_pair, "trade_type": trade_type, "amount": Decimal(str(trade_data["q"])), "price": Decimal(str(trade_data["p"])), @@ -444,9 +512,7 @@ def _process_recent_trades_event(self, event: Dict[str, Any]): content=message_content, timestamp=timestamp, ) - self._publisher.trigger_event( - event_tag=PolkadexOrderBookEvent.PublicTradeEvent, message=trade_message - ) + self._publisher.trigger_event(event_tag=OrderBookEvent.TradeEvent, message=trade_message) def _process_private_event(self, event: Dict[str, Any]): event_data = json.loads(event["websocket_streams"]["data"]) @@ -455,6 +521,8 @@ def _process_private_event(self, event: Dict[str, Any]): safe_ensure_future(self._process_balance_event(event=event_data)) elif event_data["type"] == "Order": safe_ensure_future(self._process_private_order_update_event(event=event_data)) + elif event_data["type"] == "TradeFormat": + safe_ensure_future(self._process_private_trade_event(event=event_data)) async def _process_balance_event(self, event: Dict[str, Any]): self._last_received_message_time = self._time() @@ -475,45 +543,63 @@ async def _process_balance_event(self, event: Dict[str, Any]): async def _process_private_order_update_event(self, event: Dict[str, Any]): self._last_received_message_time = self._time() - client_order_id = event["client_order_id"] exchange_order_id = event["id"] - trading_pair = event["pair"] - fee_amount = Decimal(event["fee"]) - fill_price = Decimal(event["avg_filled_price"]) + base = event["pair"]["base"]["asset"] + quote = event["pair"]["quote"]["asset"] + trading_pair = combine_to_hb_trading_pair(base=self._assets_map[base], quote=self._assets_map[quote]) fill_amount = Decimal(event["filled_quantity"]) - fill_quote_amount = Decimal(event["filled_quantity"]) + order_state = CONSTANTS.ORDER_STATE[event["status"]] - fee = TradeFeeBase.new_spot_fee( - fee_schema=TradeFeeSchema(), - trade_type=TradeType.BUY if event["side"] == "Bid" else TradeType.SELL, - flat_fees=[TokenAmount(amount=fee_amount, token=None)], + if order_state == OrderState.OPEN and fill_amount > 0: + order_state = OrderState.PARTIALLY_FILLED + order_update = OrderUpdate( + trading_pair=trading_pair, + update_timestamp=event["stid"], + new_state=order_state, + client_order_id=event["client_order_id"], + exchange_order_id=exchange_order_id, ) + self._publisher.trigger_event(event_tag=MarketEvent.OrderUpdate, message=order_update) + + async def _process_private_trade_event(self, event: Dict[str, Any]): + exchange_trading_pair = event["m"] + trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol=exchange_trading_pair) + price = Decimal(event["p"]) + size = Decimal(event["q"]) + trade_type = self._hummingbot_trade_type[event["s"]] + fee = await self._build_fee_for_event(event=event, trade_type=trade_type) trade_update = TradeUpdate( - trade_id=str(event["event_id"]), - client_order_id=client_order_id, - exchange_order_id=exchange_order_id, + trade_id=event["trade_id"], + client_order_id=event["cid"], + exchange_order_id=event["order_id"], trading_pair=trading_pair, fill_timestamp=self._time(), - fill_price=fill_price, - fill_base_amount=fill_amount, - fill_quote_amount=fill_quote_amount, + fill_price=price, + fill_base_amount=size, + fill_quote_amount=price * size, fee=fee, ) self._publisher.trigger_event(event_tag=MarketEvent.TradeUpdate, message=trade_update) - client_order_id = event["client_order_id"] - order_state = CONSTANTS.ORDER_STATE[event["status"]] - if order_state == OrderState.OPEN and fill_amount > 0: - order_state = OrderState.PARTIALLY_FILLED - order_update = OrderUpdate( - trading_pair=trading_pair, - update_timestamp=self._time(), - new_state=order_state, - client_order_id=client_order_id, - exchange_order_id=event["id"], + async def _build_fee_for_event(self, event: Dict[str, Any], trade_type: TradeType) -> TradeFeeBase: + """Builds a TradeFee object from the given event data.""" + exchange_trading_pair = event["m"] + trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol=exchange_trading_pair) + _, quote = split_hb_trading_pair(trading_pair=trading_pair) + fee = TradeFeeBase.new_spot_fee( + fee_schema=self._connector.trade_fee_schema(), + trade_type=trade_type, + percent_token=quote, + flat_fees=[TokenAmount(token=quote, amount=Decimal("0"))], # feels will be zero for the foreseeable future ) - self._publisher.trigger_event(event_tag=MarketEvent.OrderUpdate, message=order_update) + return fee def _time(self): return time.time() + + @staticmethod + def normalize_fraction(decimal_value: Decimal) -> Decimal: + normalized = decimal_value.normalize() + sign, digit, exponent = normalized.as_tuple() + return normalized if exponent <= 0 else normalized.quantize(1) diff --git a/hummingbot/connector/exchange/polkadex/polkadex_events.py b/hummingbot/connector/exchange/polkadex/polkadex_events.py deleted file mode 100644 index 42f27e1081..0000000000 --- a/hummingbot/connector/exchange/polkadex/polkadex_events.py +++ /dev/null @@ -1,6 +0,0 @@ -from enum import Enum - - -class PolkadexOrderBookEvent(int, Enum): - OrderBookDataSourceUpdateEvent = 904 - PublicTradeEvent = 905 diff --git a/hummingbot/connector/exchange/polkadex/polkadex_exchange.py b/hummingbot/connector/exchange/polkadex/polkadex_exchange.py index 5a7f321150..774ee9bf27 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_exchange.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_exchange.py @@ -1,4 +1,5 @@ import asyncio +import math from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple from _decimal import Decimal @@ -9,15 +10,16 @@ from hummingbot.connector.exchange.polkadex.polkadex_data_source import PolkadexDataSource from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule +from hummingbot.connector.utils import get_new_client_order_id from hummingbot.core.api_throttler.data_types import RateLimit from hummingbot.core.data_type.common import OrderType, TradeType -from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderUpdate, TradeUpdate +from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, OrderUpdate, TradeUpdate from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource from hummingbot.core.data_type.trade_fee import TokenAmount, TradeFeeBase from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.core.event.event_forwarder import EventForwarder from hummingbot.core.event.events import AccountEvent, BalanceUpdateEvent, MarketEvent -from hummingbot.core.network_iterator import NetworkStatus +from hummingbot.core.network_iterator import NetworkStatus, safe_ensure_future from hummingbot.core.utils.estimate_fee import build_trade_fee from hummingbot.core.web_assistant.auth import AuthBase from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -36,12 +38,13 @@ def __init__( trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN, - shallow_order_book: bool = False, # Polkadex can't support shallow order book because (no ticker endpoint) ): self._trading_required = trading_required self._trading_pairs = trading_pairs self._domain = domain - self._data_source = PolkadexDataSource(seed_phrase=polkadex_seed_phrase, domain=self._domain) + self._data_source = PolkadexDataSource( + connector=self, seed_phrase=polkadex_seed_phrase, domain=self._domain, trading_required=trading_required + ) super().__init__(client_config_map=client_config_map) self._data_source.configure_throttler(throttler=self._throttler) self._forwarders = [] @@ -111,7 +114,6 @@ async def stop_network(self): """ await super().stop_network() await self._data_source.stop() - self._forwarders = [] def supported_order_types(self) -> List[OrderType]: return [OrderType.LIMIT, OrderType.MARKET] @@ -128,26 +130,107 @@ async def check_network(self) -> NetworkStatus: status = NetworkStatus.NOT_CONNECTED return status + # === Orders placing === + + def buy(self, + trading_pair: str, + amount: Decimal, + order_type=OrderType.LIMIT, + price: Decimal = s_decimal_NaN, + **kwargs) -> str: + """ + Creates a promise to create a buy order using the parameters + + :param trading_pair: the token pair to operate with + :param amount: the order amount + :param order_type: the type of order to create (MARKET, LIMIT, LIMIT_MAKER) + :param price: the order price + + :return: the id assigned by the connector to the order (the client id) + """ + order_id = get_new_client_order_id( + is_buy=True, + trading_pair=trading_pair, + hbot_order_id_prefix=self.client_order_id_prefix, + max_id_len=self.client_order_id_max_length + ) + hex_order_id = f"0x{order_id.encode('utf-8').hex()}" + safe_ensure_future(self._create_order( + trade_type=TradeType.BUY, + order_id=hex_order_id, + trading_pair=trading_pair, + amount=amount, + order_type=order_type, + price=price, + **kwargs)) + return hex_order_id + + def sell(self, + trading_pair: str, + amount: Decimal, + order_type: OrderType = OrderType.LIMIT, + price: Decimal = s_decimal_NaN, + **kwargs) -> str: + """ + Creates a promise to create a sell order using the parameters. + :param trading_pair: the token pair to operate with + :param amount: the order amount + :param order_type: the type of order to create (MARKET, LIMIT, LIMIT_MAKER) + :param price: the order price + :return: the id assigned by the connector to the order (the client id) + """ + order_id = get_new_client_order_id( + is_buy=False, + trading_pair=trading_pair, + hbot_order_id_prefix=self.client_order_id_prefix, + max_id_len=self.client_order_id_max_length + ) + hex_order_id = f"0x{order_id.encode('utf-8').hex()}" + safe_ensure_future(self._create_order( + trade_type=TradeType.SELL, + order_id=hex_order_id, + trading_pair=trading_pair, + amount=amount, + order_type=order_type, + price=price, + **kwargs)) + return hex_order_id + def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception) -> bool: # Polkadex does not use a time synchronizer return False def _is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool: - return "Order not found" in str(status_update_exception) + return CONSTANTS.ORDER_NOT_FOUND_MESSAGE in str(status_update_exception) def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: - return str(CONSTANTS.ORDER_NOT_FOUND_ERROR_CODE) in str( - cancelation_exception - ) and CONSTANTS.ORDER_NOT_FOUND_MESSAGE in str(cancelation_exception) + return CONSTANTS.ORDER_NOT_FOUND_MESSAGE in str(cancelation_exception) + + async def _execute_order_cancel_and_process_update(self, order: InFlightOrder) -> bool: + new_order_state = await self._place_cancel(order.client_order_id, order) + cancelled = new_order_state in [OrderState.CANCELED, OrderState.PENDING_CANCEL] + if cancelled: + update_timestamp = self.current_timestamp + if update_timestamp is None or math.isnan(update_timestamp): + update_timestamp = self._time() + order_update: OrderUpdate = OrderUpdate( + client_order_id=order.client_order_id, + trading_pair=order.trading_pair, + update_timestamp=update_timestamp, + new_state=new_order_state, + ) + self._order_tracker.process_order_update(order_update) + return cancelled - async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder) -> bool: + async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder) -> OrderState: await tracked_order.get_exchange_order_id() market_symbol = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) - await self._data_source.cancel_order( + new_order_state = await self._data_source.cancel_order( order=tracked_order, market_symbol=market_symbol, timestamp=self.current_timestamp ) - return True + + return new_order_state async def _place_order( self, @@ -219,12 +302,32 @@ async def _update_balances(self): self._account_balances[token_balance_info["token_name"]] = token_balance_info["total_balance"] self._account_available_balances[token_balance_info["token_name"]] = token_balance_info["available_balance"] + async def _update_orders_fills(self, orders: List[InFlightOrder]): + try: + if len(orders) != 0: + minimum_creation_timestamp = min([order.creation_timestamp for order in orders]) + current_timestamp = self.current_timestamp + trade_updates = await self._data_source.get_all_fills( + from_timestamp=minimum_creation_timestamp, + to_timestamp=current_timestamp, + orders=orders, + ) + + for trade_update in trade_updates: + self._order_tracker.process_trade_update(trade_update=trade_update) + + except asyncio.CancelledError: + raise + except Exception: + self.logger().warning("Error fetching trades updates.") + async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: - # Polkadex does not provide an endpoint to get trades. They have to be processed from the stream updates - return [] + # not used + raise NotImplementedError async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: symbol = await self.exchange_symbol_associated_to_pair(tracked_order.trading_pair) + await tracked_order.get_exchange_order_id() order_update = await self._data_source.order_update(order=tracked_order, market_symbol=symbol) return order_update @@ -270,20 +373,12 @@ async def _initialize_trading_pair_symbol_map(self): async def _update_trading_rules(self): trading_rules_list = await self._data_source.all_trading_rules() - self._trading_rules.clear() - for trading_rule in trading_rules_list: - trading_pair = await self.trading_pair_associated_to_exchange_symbol(trading_rule.trading_pair) - new_trading_rule = TradingRule( - trading_pair=trading_pair, - min_order_size=trading_rule.min_order_size, - max_order_size=trading_rule.max_order_size, - min_price_increment=trading_rule.min_price_increment, - min_base_amount_increment=trading_rule.min_base_amount_increment, - min_quote_amount_increment=trading_rule.min_quote_amount_increment, - min_notional_size=trading_rule.min_notional_size, - min_order_value=trading_rule.min_order_value, - ) - self._trading_rules[trading_pair] = new_trading_rule + self._trading_rules = {trading_rule.trading_pair: trading_rule for trading_rule in trading_rules_list} + + async def _get_all_pairs_prices(self) -> Dict[str, Any]: + # Polkadex is configured to not be a price provider (check is_price_provider) + # This method should never be called + raise NotImplementedError # pragma: no cover async def _get_last_traded_price(self, trading_pair: str) -> float: symbol = await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) @@ -308,9 +403,8 @@ def _process_balance_event(self, event: BalanceUpdateEvent): self._account_available_balances[event.asset_name] = event.available_balance def _process_user_order_update(self, order_update: OrderUpdate): - tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get( - order_update.exchange_order_id - ) + tracked_order = self._order_tracker.all_updatable_orders.get(order_update.client_order_id) + if tracked_order is not None: self.logger().debug(f"Processing order update {order_update}\nUpdatable order {tracked_order.to_json()}") order_update_to_process = OrderUpdate( diff --git a/hummingbot/connector/exchange/polkadex/polkadex_query_executor.py b/hummingbot/connector/exchange/polkadex/polkadex_query_executor.py index 74ac694bf9..7b288fd029 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_query_executor.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_query_executor.py @@ -40,6 +40,12 @@ async def recent_trades(self, market_symbol: str, limit: int) -> Dict[str, Any]: async def get_all_balances_by_main_account(self, main_account: str) -> Dict[str, Any]: raise NotImplementedError # pragma: no cover + @abstractmethod + async def get_order_fills_by_main_account( + self, from_timestamp: float, to_timestamp: float, main_account: str + ) -> Dict[str, Any]: + raise NotImplementedError # pragma: no cover + @abstractmethod async def place_order(self, polkadex_order: Dict[str, Any], signature: Dict[str, Any]) -> Dict[str, Any]: raise NotImplementedError # pragma: no cover @@ -50,6 +56,7 @@ async def cancel_order( order_id: str, market_symbol: str, proxy_address: str, + main_address: str, signature: Dict[str, Any], ) -> Dict[str, Any]: raise NotImplementedError # pragma: no cover @@ -64,6 +71,10 @@ async def list_order_history_by_account( async def find_order_by_main_account(self, main_account: str, market_symbol: str, order_id: str) -> Dict[str, Any]: raise NotImplementedError # pragma: no cover + @abstractmethod + async def list_open_orders_by_main_account(self, main_account: str) -> Dict[str, Any]: + raise NotImplementedError # pragma: no cover + @abstractmethod async def listen_to_orderbook_updates(self, events_handler: Callable, market_symbol: str): raise NotImplementedError # pragma: no cover @@ -94,7 +105,7 @@ def __init__(self, auth: AppSyncAuthentication, domain: Optional[str] = CONSTANT async def all_assets(self): query = gql( """ - query MyQuery { + query GetAllAssets { getAllAssets { items { asset_id @@ -115,14 +126,14 @@ async def all_markets(self): query MyQuery { getAllMarkets { items { - base_asset_precision market max_order_price - max_order_qty min_order_price min_order_qty + max_order_qty price_tick_size qty_step_size + base_asset_precision quote_asset_precision } } @@ -144,7 +155,9 @@ async def get_orderbook(self, market_symbol: str) -> Dict[str, Any]: p q s + stid } + nextToken } } """ @@ -160,16 +173,19 @@ async def main_account_from_proxy(self, proxy_account=str) -> str: """ query findUserByProxyAccount($proxy_account: String!) { findUserByProxyAccount(proxy_account: $proxy_account) { - items + items { + hash_key + range_key + stid + } } } """ ) - parameters = {"proxy_account": proxy_account} result = await self._execute_query(query=query, parameters=parameters) - main_account = result["findUserByProxyAccount"]["items"][0].split(",")[2][11:-1] + main_account = result["findUserByProxyAccount"]["items"][0]["range_key"] return main_account async def recent_trades(self, market_symbol: str, limit: int) -> Dict[str, Any]: @@ -183,7 +199,6 @@ async def recent_trades(self, market_symbol: str, limit: int) -> Dict[str, Any]: p q t - sid } } } @@ -198,7 +213,7 @@ async def recent_trades(self, market_symbol: str, limit: int) -> Dict[str, Any]: async def get_all_balances_by_main_account(self, main_account: str) -> Dict[str, Any]: query = gql( """ - query getAllBalancesByMainAccount($main: String!) { + query GetAllBalancesByMainAccount($main: String!) { getAllBalancesByMainAccount(main_account: $main) { items { a @@ -215,11 +230,56 @@ async def get_all_balances_by_main_account(self, main_account: str) -> Dict[str, result = await self._execute_query(query=query, parameters=parameters) return result + async def get_order_fills_by_main_account( + self, from_timestamp: float, to_timestamp: float, main_account: str + ) -> Dict[str, Any]: + query = gql( + """ + query listTradesByMainAccount( + $main_account:String! + $limit: Int + $from: AWSDateTime! + $to: AWSDateTime! + $nextToken: String + ) { + listTradesByMainAccount( + main_account: $main_account + from: $from + to: $to + limit: $limit + nextToken: $nextToken + ) { + items { + isReverted + m + m_id + p + q + stid + t + t_id + trade_id + } + } + } + """ + ) + + parameters = { + "main_account": main_account, + "from": self._timestamp_to_aws_datetime_string(timestamp=from_timestamp), + "to": self._timestamp_to_aws_datetime_string(timestamp=to_timestamp), + } + + result = await self._execute_query(query=query, parameters=parameters) + + return result + async def place_order(self, polkadex_order: Dict[str, Any], signature: Dict[str, Any]) -> Dict[str, Any]: query = gql( """ - mutation PlaceOrder($input: UserActionInput!) { - place_order(input: $input) + mutation PlaceOrder($payload: String!) { + place_order(input: {payload: $payload}) } """ ) @@ -228,7 +288,7 @@ async def place_order(self, polkadex_order: Dict[str, Any], signature: Dict[str, polkadex_order, signature, ] - parameters = {"input": {"payload": json.dumps({"PlaceOrder": input_parameters})}} + parameters = {"payload": json.dumps({"PlaceOrder": input_parameters})} result = await self._execute_query(query=query, parameters=parameters) return result @@ -237,24 +297,26 @@ async def cancel_order( self, order_id: str, market_symbol: str, + main_address: str, proxy_address: str, signature: Dict[str, Any], ) -> Dict[str, Any]: query = gql( """ - mutation CancelOrder($input: UserActionInput!) { - cancel_order(input: $input) + mutation CancelOrder($payload: String!) { + cancel_order(input: {payload: $payload}) } """ ) input_parameters = [ order_id, + main_address, proxy_address, market_symbol, signature, ] - parameters = {"input": {"payload": json.dumps({"CancelOrder": input_parameters})}} + parameters = {"payload": json.dumps({"CancelOrder": input_parameters})} result = await self._execute_query(query=query, parameters=parameters) return result @@ -264,24 +326,35 @@ async def list_order_history_by_account( ) -> Dict[str, Any]: query = gql( """ - query ListOrderHistory($main_account: String!, $to: AWSDateTime!, $from: AWSDateTime!) { - listOrderHistorybyMainAccount(main_account: $main_account, to: $to, from: $from) { + query ListOrderHistory( + $main_account:String! + $limit: Int + $from: AWSDateTime! + $to: AWSDateTime! + $nextToken: String + ) { + listOrderHistorybyMainAccount( + main_account: $main_account + from: $from + to: $to + limit: $limit + nextToken: $nextToken + ) { items { - afp + u cid - fee - fq id - isReverted + t m + s ot + st p q - s - sid - st - t - u + afp + fq + fee + isReverted } } } @@ -290,8 +363,8 @@ async def list_order_history_by_account( parameters = { "main_account": main_account, - "to": datetime.utcfromtimestamp(to_time).isoformat(timespec="milliseconds") + "Z", - "from": datetime.utcfromtimestamp(from_time).isoformat(timespec="milliseconds") + "Z", + "to": self._timestamp_to_aws_datetime_string(timestamp=to_time), + "from": self._timestamp_to_aws_datetime_string(timestamp=from_time), } result = await self._execute_query(query=query, parameters=parameters) @@ -313,8 +386,8 @@ async def find_order_by_main_account(self, main_account: str, market_symbol: str p q s - sid st + stid t u } @@ -331,6 +404,38 @@ async def find_order_by_main_account(self, main_account: str, market_symbol: str result = await self._execute_query(query=query, parameters=parameters) return result + async def list_open_orders_by_main_account(self, main_account: str) -> Dict[str, Any]: + query = gql( + """ + query ListOpenOrdersByMainAccount($main_account: String!, $limit: Int, $nextToken: String) { + listOpenOrdersByMainAccount(main_account: $main_account, limit: $limit, nextToken: $nextToken) { + items { + u + cid + id + t + m + s + ot + st + p + q + afp + fq + fee + stid + isReverted + } + } + } + """ + ) + + parameters = {"main_account": main_account} + + result = await self._execute_query(query=query, parameters=parameters) + return result + async def listen_to_orderbook_updates(self, events_handler: Callable, market_symbol: str): while True: try: @@ -395,3 +500,8 @@ async def _subscribe_to_stream(self, stream_name: str) -> AsyncIterable: async with Client(transport=transport, fetch_schema_from_transport=False) as session: async for result in session.subscribe(query, variable_values=variables, parse_result=True): yield result + + @staticmethod + def _timestamp_to_aws_datetime_string(timestamp: float) -> str: + timestamp_string = datetime.utcfromtimestamp(timestamp).isoformat(timespec="milliseconds") + "Z" + return timestamp_string diff --git a/hummingbot/connector/exchange/polkadex/polkadex_utils.py b/hummingbot/connector/exchange/polkadex/polkadex_utils.py index 6bc97f5d74..afe091f6ab 100644 --- a/hummingbot/connector/exchange/polkadex/polkadex_utils.py +++ b/hummingbot/connector/exchange/polkadex/polkadex_utils.py @@ -9,14 +9,16 @@ EXAMPLE_PAIR = "PDEX-1" DEFAULT_FEES = TradeFeeSchema( - maker_percent_fee_decimal=Decimal("0.002"), - taker_percent_fee_decimal=Decimal("0.002"), + maker_percent_fee_decimal=Decimal("0"), + taker_percent_fee_decimal=Decimal("0"), ) def normalized_asset_name(asset_id: str, asset_name: str) -> str: name = asset_name if asset_id.isdigit() else asset_id name = name.replace("CHAINBRIDGE-", "C") + name = name.replace("TEST DEX", "TDEX") + name = name.replace("TEST BRIDGE", "TBRI") return name diff --git a/hummingbot/connector/gateway/clob_perp/data_sources/injective_perpetual/injective_perpetual_constants.py b/hummingbot/connector/gateway/clob_perp/data_sources/injective_perpetual/injective_perpetual_constants.py index 85f8424edf..33d73811f0 100644 --- a/hummingbot/connector/gateway/clob_perp/data_sources/injective_perpetual/injective_perpetual_constants.py +++ b/hummingbot/connector/gateway/clob_perp/data_sources/injective_perpetual/injective_perpetual_constants.py @@ -4,11 +4,11 @@ from typing import Dict, Tuple from pyinjective.constant import ( - Network, devnet_config as DEVNET_TOKEN_META_CONFIG, mainnet_config as MAINNET_TOKEN_META_CONFIG, testnet_config as TESTNET_TOKEN_META_CONFIG, ) +from pyinjective.core.network import Network from hummingbot.connector.constants import MINUTE, SECOND from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/clob_api_data_source_base.py b/hummingbot/connector/gateway/clob_spot/data_sources/clob_api_data_source_base.py index 385337cdc8..61069082c4 100644 --- a/hummingbot/connector/gateway/clob_spot/data_sources/clob_api_data_source_base.py +++ b/hummingbot/connector/gateway/clob_spot/data_sources/clob_api_data_source_base.py @@ -44,6 +44,7 @@ def __init__( self._forwarders_map: Dict[Tuple[Enum, Callable], EventForwarder] = {} self._gateway_order_tracker: Optional[GatewayOrderTracker] = None self._markets_info: Dict[str, Any] = {} + self.cancel_all_orders_timeout = None @property @abstractmethod @@ -223,3 +224,7 @@ def add_listener(self, event_tag: Enum, listener: EventListener): def remove_listener(self, event_tag: Enum, listener: EventListener): self._publisher.remove_listener(event_tag=event_tag, listener=listener) + + @property + def is_cancel_request_in_exchange_synchronous(self) -> bool: + return False diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_api_data_source.py b/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_api_data_source.py index 01f7a344c0..412504f328 100644 --- a/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_api_data_source.py +++ b/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_api_data_source.py @@ -11,7 +11,7 @@ from grpc.aio import UnaryStreamCall from pyinjective.async_client import AsyncClient from pyinjective.composer import Composer as ProtoMsgComposer -from pyinjective.constant import Network +from pyinjective.core.network import Network from pyinjective.proto.exchange.injective_accounts_rpc_pb2 import StreamSubaccountBalanceResponse from pyinjective.proto.exchange.injective_explorer_rpc_pb2 import GetTxByTxHashResponse, StreamTxsResponse from pyinjective.proto.exchange.injective_portfolio_rpc_pb2 import ( diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_utils.py b/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_utils.py index 52a09d7c8a..13786944a5 100644 --- a/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_utils.py +++ b/hummingbot/connector/gateway/clob_spot/data_sources/injective/injective_utils.py @@ -1,15 +1,16 @@ import logging from decimal import Decimal -from typing import List +from math import floor +from typing import List, Union from pyinjective.composer import Composer as InjectiveComposer -from pyinjective.constant import Denom, Network +from pyinjective.constant import Denom +from pyinjective.core.network import Network from pyinjective.orderhash import OrderHashResponse, build_eip712_msg, hash_order from pyinjective.proto.injective.exchange.v1beta1 import ( exchange_pb2 as injective_dot_exchange_dot_v1beta1_dot_exchange__pb2, ) from pyinjective.proto.injective.exchange.v1beta1.exchange_pb2 import DerivativeOrder, SpotOrder -from pyinjective.utils.utils import derivative_price_to_backend, derivative_quantity_to_backend from hummingbot.connector.gateway.clob_spot.data_sources.injective.injective_constants import ( ACC_NONCE_PATH_RATE_LIMIT_ID, @@ -140,3 +141,23 @@ def derivative_margin_to_backend_using_gateway_approach( res = int(numerator / denominator) return res + + +def floor_to(value: Union[float, Decimal], target: Union[float, Decimal]) -> Decimal: + value_tmp = Decimal(str(value)) + target_tmp = Decimal(str(target)) + result = int(floor(value_tmp / target_tmp)) * target_tmp + return result + + +def derivative_quantity_to_backend(quantity, denom) -> int: + quantity_tick_size = float(denom.min_quantity_tick_size) / pow(10, denom.base) + scale_quantity = Decimal(18 + denom.base) + exchange_quantity = floor_to(quantity, quantity_tick_size) * pow(Decimal(10), scale_quantity) + return int(exchange_quantity) + + +def derivative_price_to_backend(price, denom) -> int: + price_tick_size = Decimal(denom.min_price_tick_size) / pow(10, denom.quote) + exchange_price = floor_to(price, float(price_tick_size)) * pow(10, 18 + denom.quote) + return int(exchange_price) diff --git a/hummingbot/smart_components/arbitrage_executor/__init__.py b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/__init__.py similarity index 100% rename from hummingbot/smart_components/arbitrage_executor/__init__.py rename to hummingbot/connector/gateway/clob_spot/data_sources/kujira/__init__.py diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_api_data_source.py b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_api_data_source.py new file mode 100644 index 0000000000..2da7df63b2 --- /dev/null +++ b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_api_data_source.py @@ -0,0 +1,994 @@ +import asyncio +import copy +from asyncio import Task +from enum import Enum +from time import time +from typing import Any, Dict, List, Optional, Tuple + +import jsonpickle +from _decimal import Decimal +from dotmap import DotMap + +from hummingbot.client.config.config_helpers import ClientConfigAdapter +from hummingbot.connector.gateway.clob_spot.data_sources.gateway_clob_api_data_source_base import ( + GatewayCLOBAPIDataSourceBase, +) +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_constants import ( + CONNECTOR, + DELAY_BETWEEN_RETRIES, + KUJIRA_NATIVE_TOKEN, + MARKETS_UPDATE_INTERVAL, + NUMBER_OF_RETRIES, + TIMEOUT, + UPDATE_ORDER_STATUS_INTERVAL, +) +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_helpers import ( + AsyncLock, + automatic_retry_with_timeout, + convert_market_name_to_hb_trading_pair, + generate_hash, +) +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_types import OrderStatus as KujiraOrderStatus +from hummingbot.connector.gateway.common_types import CancelOrderResult, PlaceOrderResult +from hummingbot.connector.gateway.gateway_in_flight_order import GatewayInFlightOrder +from hummingbot.connector.trading_rule import TradingRule +from hummingbot.core.data_type import in_flight_order +from hummingbot.core.data_type.common import OrderType +from hummingbot.core.data_type.in_flight_order import OrderState, OrderUpdate, TradeUpdate +from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType +from hummingbot.core.data_type.trade_fee import MakerTakerExchangeFeeRates, TokenAmount, TradeFeeBase, TradeFeeSchema +from hummingbot.core.event.events import AccountEvent, MarketEvent, OrderBookDataSourceEvent, OrderCancelledEvent +from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient +from hummingbot.core.network_iterator import NetworkStatus +from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather + + +class KujiraAPIDataSource(GatewayCLOBAPIDataSourceBase): + + def __init__( + self, + trading_pairs: List[str], + connector_spec: Dict[str, Any], + client_config_map: ClientConfigAdapter, + ): + super().__init__( + trading_pairs=trading_pairs, + connector_spec=connector_spec, + client_config_map=client_config_map + ) + + self._chain = connector_spec["chain"] + self._network = connector_spec["network"] + self._connector = CONNECTOR + self._owner_address = connector_spec["wallet_address"] + self._payer_address = self._owner_address + + self._trading_pair = None + if self._trading_pairs: + self._trading_pair = self._trading_pairs[0] + + self._markets = None + self._market = None + + self._user_balances = None + + self._tasks = DotMap({ + "update_order_status_loop": None + }, _dynamic=False) + + self._locks = DotMap({ + "place_order": AsyncLock(), + "place_orders": AsyncLock(), + "cancel_order": AsyncLock(), + "cancel_orders": AsyncLock(), + "settle_market_funds": AsyncLock(), + "settle_markets_funds": AsyncLock(), + "settle_all_markets_funds": AsyncLock(), + "all_active_orders": AsyncLock(), + }, _dynamic=False) + + self._gateway = GatewayHttpClient.get_instance(self._client_config) + + self._all_active_orders = None + + self._snapshots_min_update_interval = 30 + self._snapshots_max_update_interval = 60 + self.cancel_all_orders_timeout = TIMEOUT + + @property + def connector_name(self) -> str: + return CONNECTOR + + @property + def real_time_balance_update(self) -> bool: + return False + + @property + def events_are_streamed(self) -> bool: + return False + + @staticmethod + def supported_stream_events() -> List[Enum]: + return [ + MarketEvent.TradeUpdate, + MarketEvent.OrderUpdate, + MarketEvent.OrderFilled, + AccountEvent.BalanceEvent, + OrderBookDataSourceEvent.TRADE_EVENT, + OrderBookDataSourceEvent.DIFF_EVENT, + OrderBookDataSourceEvent.SNAPSHOT_EVENT, + ] + + def get_supported_order_types(self) -> List[OrderType]: + return [OrderType.LIMIT, OrderType.MARKET] + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def start(self): + self.logger().setLevel("INFO") + self.logger().debug("start: start") + + await super().start() + + self._tasks.update_order_status_loop = self._tasks.update_order_status_loop \ + or safe_ensure_future( + coro=self._update_all_active_orders() + ) + + self.logger().debug("start: end") + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def stop(self): + self.logger().debug("stop: start") + + await super().stop() + + self._tasks.update_order_status_loop and self._tasks.update_order_status_loop.cancel() + self._tasks.update_order_status_loop = None + + self.logger().debug("stop: end") + + async def place_order(self, order: GatewayInFlightOrder, **kwargs) -> Tuple[Optional[str], Optional[Dict[str, Any]]]: + self.logger().debug("place_order: start") + + self._check_markets_initialized() or await self._update_markets() + + async with self._locks.place_order: + try: + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + "trading_pair": self._trading_pair, + "address": self._owner_address, + "trade_type": order.trade_type, + "order_type": order.order_type, + "price": order.price, + "size": order.amount, + "client_order_id": order.client_order_id, + } + + self.logger().debug(f"""clob_place_order request:\n "{self._dump(request)}".""") + + response = await self._gateway_clob_place_order(request) + + self.logger().debug(f"""clob_place_order response:\n "{self._dump(response)}".""") + + transaction_hash = response["txHash"] + + order.exchange_order_id = response["id"] + + order.current_state = OrderState.CREATED + + self.logger().info( + f"""Order "{order.client_order_id}" / "{order.exchange_order_id}" successfully placed. Transaction hash: "{transaction_hash}".""" + ) + except Exception as exception: + self.logger().info( + f"""Placement of order "{order.client_order_id}" failed.""" + ) + + raise exception + + if transaction_hash in (None, ""): + raise Exception( + f"""Placement of order "{order.client_order_id}" failed. Invalid transaction hash: "{transaction_hash}".""" + ) + + misc_updates = DotMap({ + "creation_transaction_hash": transaction_hash, + }, _dynamic=False) + + self.logger().debug("place_order: end") + + await self._update_order_status() + + return order.exchange_order_id, misc_updates + + async def batch_order_create(self, orders_to_create: List[GatewayInFlightOrder]) -> List[PlaceOrderResult]: + self.logger().debug("batch_order_create: start") + + self._check_markets_initialized() or await self._update_markets() + + candidate_orders = [in_flight_order] + client_ids = [] + for order_to_create in orders_to_create: + if not order_to_create.client_order_id: + order_to_create.client_order_id = generate_hash(order_to_create) + client_ids.append(order_to_create.client_order_id) + + candidate_order = in_flight_order.InFlightOrder( + amount=order_to_create.amount, + client_order_id=order_to_create.client_order_id, + creation_timestamp=0, + order_type=order_to_create.order_type, + trade_type=order_to_create.trade_type, + trading_pair=self._trading_pair, + ) + candidate_orders.append(candidate_order) + + async with self._locks.place_orders: + try: + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + "address": self._owner_address, + "orders_to_create": candidate_orders, + "orders_to_cancel": [], + } + + self.logger().debug(f"""clob_batch_order_modify request:\n "{self._dump(request)}".""") + + response = await self._gateway_clob_batch_order_modify(request) + + self.logger().debug(f"""clob_batch_order_modify response:\n "{self._dump(response)}".""") + + transaction_hash = response["txHash"] + + self.logger().info( + f"""Orders "{client_ids}" successfully placed. Transaction hash: {transaction_hash}.""" + ) + except Exception as exception: + self.logger().info( + f"""Placement of orders "{client_ids}" failed.""" + ) + + raise exception + + if transaction_hash in (None, ""): + raise RuntimeError( + f"""Placement of orders "{client_ids}" failed. Invalid transaction hash: "{transaction_hash}".""" + ) + + place_order_results = [] + for order_to_create, exchange_order_id in zip(orders_to_create, response["ids"]): + order_to_create.exchange_order_id = None + + place_order_results.append(PlaceOrderResult( + update_timestamp=time(), + client_order_id=order_to_create.client_order_id, + exchange_order_id=exchange_order_id, + trading_pair=order_to_create.trading_pair, + misc_updates={ + "creation_transaction_hash": transaction_hash, + }, + exception=None, + )) + + self.logger().debug("batch_order_create: end") + + return place_order_results + + async def cancel_order(self, order: GatewayInFlightOrder) -> Tuple[bool, Optional[Dict[str, Any]]]: + active_order = self._gateway_order_tracker.active_orders.get(order.client_order_id) + + if active_order.exchange_order_id is None: + await self._update_order_status() + active_order = self._gateway_order_tracker.active_orders.get(order.client_order_id) + + fillable = self._gateway_order_tracker.all_fillable_orders_by_exchange_order_id.get( + active_order.exchange_order_id + ) + + if fillable and ( + active_order + ) and ( + active_order.current_state != OrderState.CANCELED + ) and ( + active_order.current_state != OrderState.FILLED + ) and ( + active_order.exchange_order_id + ): + self.logger().debug("cancel_order: start") + + self._check_markets_initialized() or await self._update_markets() + + await order.get_exchange_order_id() + + transaction_hash = None + + async with self._locks.cancel_order: + try: + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + "trading_pair": order.trading_pair, + "address": self._owner_address, + "exchange_order_id": order.exchange_order_id, + } + + self.logger().debug(f"""clob_cancel_order request:\n "{self._dump(request)}".""") + + response = await self._gateway_clob_cancel_order(request) + + self.logger().debug(f"""clob_cancel_order response:\n "{self._dump(response)}".""") + + transaction_hash = response["txHash"] + + if transaction_hash in ("", None): + return False, DotMap({}, _dynamic=False) + + self.logger().info( + f"""Order "{order.client_order_id}" / "{order.exchange_order_id}" successfully cancelled. Transaction hash: "{transaction_hash}".""" + ) + except Exception as exception: + # await self.gateway_order_tracker.process_order_not_found(order.client_order_id) + if f"""Order "{order.exchange_order_id}" not found on markets""" in str(exception.args): + # order_update = self.get_order_status_update(order) + # self.gateway_order_tracker.process_order_update(order_update) + + self.logger().info( + f"""Order "{order.exchange_order_id}" not found on markets""" + ) + + return True, DotMap({}, _dynamic=False) + + elif 'No orders with the specified information exist' in str(exception.args): + self.logger().info( + f"""Order "{order.client_order_id}" / "{order.exchange_order_id}" already cancelled.""" + ) + + transaction_hash = "0000000000000000000000000000000000000000000000000000000000000000" # noqa: mock + else: + self.logger().info( + f"""Cancellation of order "{order.client_order_id}" / "{order.exchange_order_id}" failed.""" + ) + + raise exception + + misc_updates = DotMap({ + "cancelation_transaction_hash": transaction_hash, + }, _dynamic=False) + + self.logger().debug("cancel_order: end") + + order.cancel_tx_hash = transaction_hash + + await self._update_order_status() + + return True, misc_updates + + return False, DotMap({}, _dynamic=False) + + async def batch_order_cancel(self, orders_to_cancel: List[GatewayInFlightOrder]) -> List[CancelOrderResult]: + self.logger().debug("batch_order_cancel: start") + + self._check_markets_initialized() or await self._update_markets() + + client_ids = [order.client_order_id for order in orders_to_cancel] + + in_flight_orders_to_cancel = [ + self._gateway_order_tracker.fetch_tracked_order(client_order_id=order.client_order_id) + for order in orders_to_cancel + ] + exchange_order_ids_to_cancel = await safe_gather( + *[order.get_exchange_order_id() for order in in_flight_orders_to_cancel], + return_exceptions=True, + ) + found_orders_to_cancel = [ + order + for order, result in zip(orders_to_cancel, exchange_order_ids_to_cancel) + if not isinstance(result, asyncio.TimeoutError) + ] + + ids = [order.exchange_order_id for order in found_orders_to_cancel] + + async with self._locks.cancel_orders: + try: + + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + "address": self._owner_address, + "orders_to_create": [], + "orders_to_cancel": found_orders_to_cancel, + } + + self.logger().debug(f"""clob_batch_order_moodify request:\n "{self._dump(request)}".""") + + response = await self._gateway_clob_batch_order_modify(request) + + self.logger().debug(f"""clob_batch_order_modify response:\n "{self._dump(response)}".""") + + transaction_hash = response["txHash"] + + self.logger().info( + f"""Orders "{client_ids}" / "{ids}" successfully cancelled. Transaction hash(es): "{transaction_hash}".""" + ) + except Exception as exception: + self.logger().info( + f"""Cancellation of orders "{client_ids}" / "{ids}" failed.""" + ) + + raise exception + + if transaction_hash in (None, ""): + raise RuntimeError( + f"""Cancellation of orders "{client_ids}" / "{ids}" failed. Invalid transaction hash: "{transaction_hash}".""" + ) + + cancel_order_results = [] + for order_to_cancel in orders_to_cancel: + cancel_order_results.append(CancelOrderResult( + client_order_id=order_to_cancel.client_order_id, + trading_pair=order_to_cancel.trading_pair, + misc_updates={ + "cancelation_transaction_hash": transaction_hash + }, + exception=None, + )) + + self.logger().debug("batch_order_cancel: end") + + return cancel_order_results + + async def get_last_traded_price(self, trading_pair: str) -> Decimal: + self.logger().debug("get_last_traded_price: start") + + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + "trading_pair": self._trading_pair, + } + + self.logger().debug(f"""get_clob_ticker request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_clob_ticker(request) + + self.logger().debug(f"""get_clob_ticker response:\n "{self._dump(response)}".""") + + ticker = DotMap(response, _dynamic=False).markets[self._trading_pair] + + ticker_price = Decimal(ticker.price) + + self.logger().debug("get_last_traded_price: end") + + return ticker_price + + async def get_order_book_snapshot(self, trading_pair: str) -> OrderBookMessage: + self.logger().debug("get_order_book_snapshot: start") + + request = { + "trading_pair": self._trading_pair, + "connector": self._connector, + "chain": self._chain, + "network": self._network, + } + + self.logger().debug(f"""get_clob_orderbook_snapshot request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_clob_orderbook_snapshot(request) + + self.logger().debug(f"""get_clob_orderbook_snapshot response:\n "{self._dump(response)}".""") + + order_book = DotMap(response, _dynamic=False) + + price_scale = 1 + size_scale = 1 + + timestamp = time() + + bids = [] + asks = [] + for bid in order_book.buys: + bids.append((Decimal(bid.price) * price_scale, Decimal(bid.quantity) * size_scale)) + + for ask in order_book.sells: + asks.append((Decimal(ask.price) * price_scale, Decimal(ask.quantity) * size_scale)) + + snapshot = OrderBookMessage( + message_type=OrderBookMessageType.SNAPSHOT, + content={ + "trading_pair": trading_pair, + "update_id": timestamp, + "bids": bids, + "asks": asks, + }, + timestamp=timestamp + ) + + self.logger().debug("get_order_book_snapshot: end") + + return snapshot + + async def get_account_balances(self) -> Dict[str, Dict[str, Decimal]]: + self.logger().debug("get_account_balances: start") + + request = { + "chain": self._chain, + "network": self._network, + "address": self._owner_address, + "connector": self._connector, + } + + if self._trading_pair: + request["token_symbols"] = [self._trading_pair.split("-")[0], self._trading_pair.split("-")[1], KUJIRA_NATIVE_TOKEN] + else: + request["token_symbols"] = [] + + # self.logger().debug(f"""get_balances request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_balances(request) + + self.logger().debug(f"""get_balances response:\n "{self._dump(response)}".""") + + balances = DotMap(response, _dynamic=False).balances + + hb_balances = {} + for token, balance in balances.items(): + balance = Decimal(balance) + hb_balances[token] = DotMap({}, _dynamic=False) + hb_balances[token]["total_balance"] = balance + hb_balances[token]["available_balance"] = balance + + # self.logger().debug("get_account_balances: end") + + return hb_balances + + async def get_order_status_update(self, in_flight_order: GatewayInFlightOrder) -> OrderUpdate: + active_order = self.gateway_order_tracker.active_orders.get(in_flight_order.client_order_id) + + if active_order: + self.logger().debug("get_order_status_update: start") + + if active_order.current_state != OrderState.CANCELED: + await in_flight_order.get_exchange_order_id() + + request = { + "trading_pair": self._trading_pair, + "chain": self._chain, + "network": self._network, + "connector": self._connector, + "address": self._owner_address, + "exchange_order_id": in_flight_order.exchange_order_id, + } + + self.logger().debug(f"""get_clob_order_status_updates request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_clob_order_status_updates(request) + + self.logger().debug(f"""get_clob_order_status_updates response:\n "{self._dump(response)}".""") + + order_response = DotMap(response, _dynamic=False)["orders"] + order_update: OrderUpdate + if order_response: + order = order_response[0] + if order: + order_status = KujiraOrderStatus.to_hummingbot(KujiraOrderStatus.from_name(order.state)) + else: + order_status = in_flight_order.current_state + + open_update = OrderUpdate( + trading_pair=in_flight_order.trading_pair, + update_timestamp=time(), + new_state=order_status, + client_order_id=in_flight_order.client_order_id, + exchange_order_id=in_flight_order.exchange_order_id, + misc_updates={ + "creation_transaction_hash": in_flight_order.creation_transaction_hash, + "cancelation_transaction_hash": in_flight_order.cancel_tx_hash, + }, + ) + + order_update = open_update + else: + canceled_update = OrderUpdate( + trading_pair=in_flight_order.trading_pair, + update_timestamp=time(), + new_state=OrderState.CANCELED, + client_order_id=in_flight_order.client_order_id, + exchange_order_id=in_flight_order.exchange_order_id, + misc_updates={ + "creation_transaction_hash": in_flight_order.creation_transaction_hash, + "cancelation_transaction_hash": in_flight_order.cancel_tx_hash, + }, + ) + + order_update = canceled_update + + self.logger().debug("get_order_status_update: end") + return order_update + + no_update = OrderUpdate( + trading_pair=in_flight_order.trading_pair, + update_timestamp=time(), + new_state=in_flight_order.current_state, + client_order_id=in_flight_order.client_order_id, + exchange_order_id=in_flight_order.exchange_order_id, + misc_updates={ + "creation_transaction_hash": in_flight_order.creation_transaction_hash, + "cancelation_transaction_hash": in_flight_order.cancel_tx_hash, + }, + ) + self.logger().debug("get_order_status_update: end") + return no_update + + async def get_all_order_fills(self, in_flight_order: GatewayInFlightOrder) -> List[TradeUpdate]: + if in_flight_order.exchange_order_id: + active_order = self.gateway_order_tracker.active_orders.get(in_flight_order.client_order_id) + + if active_order: + if active_order.current_state != OrderState.CANCELED: + self.logger().debug("get_all_order_fills: start") + + trade_update = None + + request = { + "trading_pair": self._trading_pair, + "chain": self._chain, + "network": self._network, + "connector": self._connector, + "address": self._owner_address, + "exchange_order_id": in_flight_order.exchange_order_id, + } + + self.logger().debug(f"""get_clob_order_status_updates request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_clob_order_status_updates(request) + + self.logger().debug(f"""get_clob_order_status_updates response:\n "{self._dump(response)}".""") + + orders = DotMap(response, _dynamic=False)["orders"] + + order = None + if len(orders): + order = orders[0] + + if order is not None: + order_status = KujiraOrderStatus.to_hummingbot(KujiraOrderStatus.from_name(order.state)) + else: + order_status = in_flight_order.current_state + + if order and order_status == OrderState.FILLED: + timestamp = time() + trade_id = str(timestamp) + + trade_update = TradeUpdate( + trade_id=trade_id, + client_order_id=in_flight_order.client_order_id, + exchange_order_id=in_flight_order.exchange_order_id, + trading_pair=in_flight_order.trading_pair, + fill_timestamp=timestamp, + fill_price=in_flight_order.price, + fill_base_amount=in_flight_order.amount, + fill_quote_amount=in_flight_order.price * in_flight_order.amount, + fee=TradeFeeBase.new_spot_fee( + fee_schema=TradeFeeSchema(), + trade_type=in_flight_order.trade_type, + flat_fees=[TokenAmount( + amount=Decimal(self._market.fees.taker), + token=self._market.quoteToken.symbol + )] + ), + ) + + self.logger().debug("get_all_order_fills: end") + + if trade_update: + return [trade_update] + + return [] + + def _get_trading_pair_from_market_info(self, market_info: Dict[str, Any]) -> str: + return market_info["hb_trading_pair"] + + def _get_exchange_base_quote_tokens_from_market_info(self, market_info: Dict[str, Any]) -> Tuple[str, str]: + base = market_info["baseToken"]["symbol"] + quote = market_info["quoteToken"]["symbol"] + + return base, quote + + def _get_last_trade_price_from_ticker_data(self, ticker_data: List[Dict[str, Any]]) -> Decimal: + raise NotImplementedError + + def is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool: + self.logger().debug("is_order_not_found_during_status_update_error: start") + + output = str(status_update_exception).startswith("No update found for order") + + self.logger().debug("is_order_not_found_during_status_update_error: end") + + return output + + def is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: + self.logger().debug("is_order_not_found_during_cancelation_error: start") + + output = False + + self.logger().debug("is_order_not_found_during_cancelation_error: end") + + return output + + async def check_network_status(self) -> NetworkStatus: + # self.logger().debug("check_network_status: start") + + try: + status = await self._gateway_ping_gateway() + + if status: + return NetworkStatus.CONNECTED + else: + return NetworkStatus.NOT_CONNECTED + except asyncio.CancelledError: + raise + except Exception as exception: + self.logger().error(exception) + + return NetworkStatus.NOT_CONNECTED + + # self.logger().debug("check_network_status: end") + + @property + def is_cancel_request_in_exchange_synchronous(self) -> bool: + self.logger().debug("is_cancel_request_in_exchange_synchronous: start") + + output = True + + self.logger().debug("is_cancel_request_in_exchange_synchronous: end") + + return output + + def _check_markets_initialized(self) -> bool: + # self.logger().debug("_check_markets_initialized: start") + + output = self._markets is not None and bool(self._markets) + + # self.logger().debug("_check_markets_initialized: end") + + return output + + async def _update_markets(self): + self.logger().debug("_update_markets: start") + + request = { + "connector": self._connector, + "chain": self._chain, + "network": self._network, + } + + if self._trading_pair: + request["trading_pair"] = self._trading_pair + + self.logger().debug(f"""get_clob_markets request:\n "{self._dump(request)}".""") + + response = await self._gateway_get_clob_markets(request) + + self.logger().debug(f"""get_clob_markets response:\n "{self._dump(response)}".""") + + if 'trading_pair' in request or self._trading_pair: + markets = DotMap(response, _dynamic=False).markets + self._markets = markets[request['trading_pair']] + self._market = self._markets + self._markets_info.clear() + self._market["hb_trading_pair"] = convert_market_name_to_hb_trading_pair(self._market.name) + self._markets_info[self._market["hb_trading_pair"]] = self._market + else: + self._markets = DotMap(response, _dynamic=False).markets + + self._markets_info.clear() + for market in self._markets.values(): + market["hb_trading_pair"] = convert_market_name_to_hb_trading_pair(market.name) + self._markets_info[market["hb_trading_pair"]] = market + + self.logger().debug("_update_markets: end") + + return self._markets + + def _parse_trading_rule(self, trading_pair: str, market_info: Any) -> TradingRule: + self.logger().debug("_parse_trading_rule: start") + + trading_rule = TradingRule( + trading_pair=trading_pair, + min_order_size=Decimal(market_info.minimumOrderSize), + min_price_increment=Decimal(market_info.minimumPriceIncrement), + min_base_amount_increment=Decimal(market_info.minimumBaseAmountIncrement), + min_quote_amount_increment=Decimal(market_info.minimumQuoteAmountIncrement), + ) + + self.logger().debug("_parse_trading_rule: end") + + return trading_rule + + def _get_exchange_trading_pair_from_market_info(self, market_info: Any) -> str: + self.logger().debug("_get_exchange_trading_pair_from_market_info: start") + + output = market_info.id + + self.logger().debug("_get_exchange_trading_pair_from_market_info: end") + + return output + + def _get_maker_taker_exchange_fee_rates_from_market_info(self, market_info: Any) -> MakerTakerExchangeFeeRates: + self.logger().debug("_get_maker_taker_exchange_fee_rates_from_market_info: start") + + fee_scaler = Decimal("1") - Decimal(market_info.fees.serviceProvider) + maker_fee = Decimal(market_info.fees.maker) * fee_scaler + taker_fee = Decimal(market_info.fees.taker) * fee_scaler + + output = MakerTakerExchangeFeeRates( + maker=maker_fee, + taker=taker_fee, + maker_flat_fees=[], + taker_flat_fees=[] + ) + + self.logger().debug("_get_maker_taker_exchange_fee_rates_from_market_info: end") + + return output + + async def _update_markets_loop(self): + self.logger().debug("_update_markets_loop: start") + + while True: + self.logger().debug("_update_markets_loop: start loop") + + await self._update_markets() + await asyncio.sleep(MARKETS_UPDATE_INTERVAL) + + self.logger().debug("_update_markets_loop: end loop") + + async def _check_if_order_failed_based_on_transaction( + self, + transaction: Any, + order: GatewayInFlightOrder + ) -> bool: + order_id = await order.get_exchange_order_id() + + return order_id.lower() not in transaction.data.lower() + + @staticmethod + def _dump(target: Any): + try: + return jsonpickle.encode(target, unpicklable=True, indent=2) + except (Exception,): + return target + + @staticmethod + def _create_task(target: Any) -> Task: + return asyncio.ensure_future(target) + + @staticmethod + def _create_event_loop(): + return asyncio.get_event_loop() + + def _create_and_run_task(self, target: Any): + event_loop = self._create_event_loop() + task = self._create_task(target) + if not event_loop.is_running(): + event_loop.run_until_complete(task) + + async def _update_order_status(self): + async with self._locks.all_active_orders: + self._all_active_orders = ( + self._gateway_order_tracker.active_orders if self._gateway_order_tracker else {} + ) + + orders = copy.copy(self._all_active_orders).values() + + for order in orders: + request = { + "trading_pair": self._trading_pair, + "chain": self._chain, + "network": self._network, + "connector": self._connector, + "address": self._owner_address, + "exchange_order_id": order.exchange_order_id, + } + + response = await self._gateway_get_clob_order_status_updates(request) + + try: + if response["orders"] is not None and len(response['orders']) and response["orders"][0] is not None and response["orders"][0]["state"] != order.current_state: + updated_order = response["orders"][0] + + message = { + "trading_pair": self._trading_pair, + "update_timestamp": + updated_order["updatedAt"] if len(updated_order["updatedAt"]) else time(), + "new_state": updated_order["state"], + } + + if updated_order["state"] in { + OrderState.PENDING_CREATE, + OrderState.OPEN, + OrderState.PARTIALLY_FILLED, + OrderState.PENDING_CANCEL, + }: + + self._publisher.trigger_event(event_tag=MarketEvent.OrderUpdate, message=message) + + elif updated_order["state"] == OrderState.FILLED.name: + + message = { + "timestamp": + updated_order["updatedAt"] if len(updated_order["updatedAt"]) else time(), + "order_id": order.client_order_id, + "trading_pair": self._trading_pair, + "trade_type": order.trade_type, + "order_type": order.order_type, + "price": order.price, + "amount": order.amount, + "trade_fee": '', + "exchange_trade_id": "", + "exchange_order_id": order.exchange_order_id, + } + + self._publisher.trigger_event(event_tag=MarketEvent.OrderFilled, message=message) + + elif updated_order["state"] == OrderState.CANCELED.name: + + message = { + "timestamp": + updated_order["updatedAt"] if len(updated_order["updatedAt"]) else time(), + "order_id": order.client_order_id, + "exchange_order_id": order.exchange_order_id, + } + + self._publisher.trigger_event(event_tag=OrderCancelledEvent, message=message) + + except Exception: + raise self.logger().exception(Exception) + + async def _update_all_active_orders(self): + while True: + await self._update_order_status() + await asyncio.sleep(UPDATE_ORDER_STATUS_INTERVAL) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_ping_gateway(self, _request=None): + return await self._gateway.ping_gateway() + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_get_clob_markets(self, request): + return await self._gateway.get_clob_markets(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_get_clob_orderbook_snapshot(self, request): + return await self._gateway.get_clob_orderbook_snapshot(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_get_clob_ticker(self, request): + return await self._gateway.get_clob_ticker(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_get_balances(self, request): + return await self._gateway.get_balances(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_clob_place_order(self, request): + return await self._gateway.clob_place_order(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_clob_cancel_order(self, request): + return await self._gateway.clob_cancel_order(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_clob_batch_order_modify(self, request): + return await self._gateway.clob_batch_order_modify(**request) + + @automatic_retry_with_timeout(retries=NUMBER_OF_RETRIES, delay=DELAY_BETWEEN_RETRIES, timeout=TIMEOUT) + async def _gateway_get_clob_order_status_updates(self, request): + return await self._gateway.get_clob_order_status_updates(**request) diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_constants.py b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_constants.py new file mode 100644 index 0000000000..d7372da29c --- /dev/null +++ b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_constants.py @@ -0,0 +1,17 @@ +from dotmap import DotMap + +KUJIRA_NATIVE_TOKEN = DotMap({ + "id": "ukuji", + "name": "Kuji", + "symbol": "KUJI", + "decimals": "6", +}, _dynamic=False) + +CONNECTOR = "kujira" + +MARKETS_UPDATE_INTERVAL = 8 * 60 * 60 +UPDATE_ORDER_STATUS_INTERVAL = 1 + +NUMBER_OF_RETRIES = 3 +DELAY_BETWEEN_RETRIES = 3 +TIMEOUT = 60 diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_helpers.py b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_helpers.py new file mode 100644 index 0000000000..933764b8f7 --- /dev/null +++ b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_helpers.py @@ -0,0 +1,75 @@ +import asyncio +import hashlib +import traceback +from datetime import datetime +from typing import Any, List + +import jsonpickle + + +def generate_hash(input: Any) -> str: + return generate_hashes([input])[0] + + +def generate_hashes(inputs: List[Any]) -> List[str]: + hashes = [] + salt = datetime.now() + + for input in inputs: + serialized = jsonpickle.encode(input, unpicklable=True) + hasher = hashlib.md5() + target = f"{salt}{serialized}".encode("utf-8") + hasher.update(target) + hash = hasher.hexdigest() + + hashes.append(hash) + + return hashes + + +def convert_hb_trading_pair_to_market_name(trading_pair: str) -> str: + return trading_pair.replace("-", "/") + + +def convert_market_name_to_hb_trading_pair(market_name: str) -> str: + return market_name.replace("/", "-") + + +def automatic_retry_with_timeout(retries=0, delay=0, timeout=None): + def decorator(function): + async def wrapper(*args, **kwargs): + errors = [] + + for i in range(retries + 1): + try: + result = await asyncio.wait_for(function(*args, **kwargs), timeout=timeout) + + return result + except Exception as e: + tb_str = traceback.format_exception(type(e), value=e, tb=e.__traceback__) + errors.append(''.join(tb_str)) + + if i < retries: + await asyncio.sleep(delay) + + error_message = f"Function failed after {retries} attempts. Here are the errors:\n" + "\n".join(errors) + + raise Exception(error_message) + + wrapper.original = function + + return wrapper + + return decorator + + +class AsyncLock: + def __init__(self): + self._lock = asyncio.Lock() + + async def __aenter__(self): + await self._lock.acquire() + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + self._lock.release() diff --git a/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_types.py b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_types.py new file mode 100644 index 0000000000..4469acb6b7 --- /dev/null +++ b/hummingbot/connector/gateway/clob_spot/data_sources/kujira/kujira_types.py @@ -0,0 +1,132 @@ +from enum import Enum + +from hummingbot.core.data_type.common import OrderType as HummingBotOrderType, TradeType as HummingBotOrderSide +from hummingbot.core.data_type.in_flight_order import OrderState as HummingBotOrderStatus + + +class OrderStatus(Enum): + OPEN = "OPEN", + CANCELLED = "CANCELLED", + PARTIALLY_FILLED = "PARTIALLY_FILLED", + FILLED = "FILLED", + CREATION_PENDING = "CREATION_PENDING", + CANCELLATION_PENDING = "CANCELLATION_PENDING", + UNKNOWN = "UNKNOWN" + + @staticmethod + def from_name(name: str): + if name == "OPEN": + return OrderStatus.OPEN + elif name == "CANCELLED": + return OrderStatus.CANCELLED + elif name == "PARTIALLY_FILLED": + return OrderStatus.PARTIALLY_FILLED + elif name == "FILLED": + return OrderStatus.FILLED + elif name == "CREATION_PENDING": + return OrderStatus.CREATION_PENDING + elif name == "CANCELLATION_PENDING": + return OrderStatus.CANCELLATION_PENDING + else: + raise ValueError(f"Unknown order status: {name}") + + @staticmethod + def from_hummingbot(target: HummingBotOrderStatus): + if target == HummingBotOrderStatus.PENDING_CREATE: + return OrderStatus.CREATION_PENDING + elif target == HummingBotOrderStatus.OPEN: + return OrderStatus.OPEN + elif target == HummingBotOrderStatus.PENDING_CANCEL: + return OrderStatus.CANCELLATION_PENDING + elif target == HummingBotOrderStatus.CANCELED: + return OrderStatus.CANCELLED + elif target == HummingBotOrderStatus.PARTIALLY_FILLED: + return OrderStatus.PARTIALLY_FILLED + elif target == HummingBotOrderStatus.FILLED: + return OrderStatus.FILLED + else: + raise ValueError(f"Unknown order status: {target}") + + @staticmethod + def to_hummingbot(self): + if self == OrderStatus.OPEN: + return HummingBotOrderStatus.OPEN + elif self == OrderStatus.CANCELLED: + return HummingBotOrderStatus.CANCELED + elif self == OrderStatus.PARTIALLY_FILLED: + return HummingBotOrderStatus.PARTIALLY_FILLED + elif self == OrderStatus.FILLED: + return HummingBotOrderStatus.FILLED + elif self == OrderStatus.CREATION_PENDING: + return HummingBotOrderStatus.PENDING_CREATE + elif self == OrderStatus.CANCELLATION_PENDING: + return HummingBotOrderStatus.PENDING_CANCEL + else: + raise ValueError(f"Unknown order status: {self}") + + +class OrderType(Enum): + MARKET = 'MARKET', + LIMIT = 'LIMIT', + + @staticmethod + def from_name(name: str): + if name == "MARKET": + return OrderType.MARKET + elif name == "LIMIT": + return OrderType.LIMIT + else: + raise ValueError(f"Unknown order type: {name}") + + @staticmethod + def from_hummingbot(target: HummingBotOrderType): + if target == HummingBotOrderType.LIMIT: + return OrderType.LIMIT + else: + raise ValueError(f'Unrecognized order type "{target}".') + + @staticmethod + def to_hummingbot(self): + if self == OrderType.LIMIT: + return HummingBotOrderType.LIMIT + else: + raise ValueError(f'Unrecognized order type "{self}".') + + +class OrderSide(Enum): + BUY = 'BUY', + SELL = 'SELL', + + @staticmethod + def from_name(name: str): + if name == "BUY": + return OrderSide.BUY + elif name == "SELL": + return OrderSide.SELL + else: + raise ValueError(f"Unknown order side: {name}") + + @staticmethod + def from_hummingbot(target: HummingBotOrderSide): + if target == HummingBotOrderSide.BUY: + return OrderSide.BUY + elif target == HummingBotOrderSide.SELL: + return OrderSide.SELL + else: + raise ValueError(f'Unrecognized order side "{target}".') + + def to_hummingbot(self): + if self == OrderSide.BUY: + return HummingBotOrderSide.BUY + elif self == OrderSide.SELL: + return HummingBotOrderSide.SELL + else: + raise ValueError(f'Unrecognized order side "{self}".') + + +class TickerSource(Enum): + ORDER_BOOK_SAP = "orderBookSimpleAveragePrice", + ORDER_BOOK_WAP = "orderBookWeightedAveragePrice", + ORDER_BOOK_VWAP = "orderBookVolumeWeightedAveragePrice", + LAST_FILLED_ORDER = "lastFilledOrder", + NOMICS = "nomics", diff --git a/hummingbot/connector/gateway/clob_spot/gateway_clob_spot.py b/hummingbot/connector/gateway/clob_spot/gateway_clob_spot.py index 74f7cac85d..06510d928e 100644 --- a/hummingbot/connector/gateway/clob_spot/gateway_clob_spot.py +++ b/hummingbot/connector/gateway/clob_spot/gateway_clob_spot.py @@ -70,6 +70,8 @@ def __init__( self._add_forwarders() + self.has_started = False + super().__init__(client_config_map) @property @@ -121,7 +123,7 @@ def trading_pairs(self) -> List[str]: @property def is_cancel_request_in_exchange_synchronous(self) -> bool: - return False + return self._api_data_source.is_cancel_request_in_exchange_synchronous @property def is_trading_required(self) -> bool: @@ -152,13 +154,29 @@ def status_dict(self) -> Dict[str, bool]: sd["api_data_source_initialized"] = self._api_data_source.ready return sd + def start(self, *args, **kwargs): + super().start(**kwargs) + safe_ensure_future(self.start_network()) + safe_ensure_future(self._api_data_source.start()) + + def stop(self, *args, **kwargs): + super().stop(**kwargs) + safe_ensure_future(self._api_data_source.stop()) + async def start_network(self): - await self._api_data_source.start() - await super().start_network() + if not self.has_started: + await self._api_data_source.start() + await super().start_network() + self.has_started = True async def stop_network(self): await super().stop_network() await self._api_data_source.stop() + self.has_started = False + + @property + def ready(self) -> bool: + return super().ready def supported_order_types(self) -> List[OrderType]: return self._api_data_source.get_supported_order_types() @@ -719,3 +737,10 @@ def _get_poll_interval(self, timestamp: float) -> float: else self.LONG_POLL_INTERVAL ) return poll_interval + + async def cancel_all(self, timeout_seconds: float) -> List[CancellationResult]: + timeout = self._api_data_source.cancel_all_orders_timeout \ + if self._api_data_source.cancel_all_orders_timeout is not None \ + else timeout_seconds + + return await super().cancel_all(timeout) diff --git a/hummingbot/core/api_throttler/async_throttler_base.py b/hummingbot/core/api_throttler/async_throttler_base.py index e8c092fd5d..d293fcd56f 100644 --- a/hummingbot/core/api_throttler/async_throttler_base.py +++ b/hummingbot/core/api_throttler/async_throttler_base.py @@ -17,6 +17,7 @@ class AsyncThrottlerBase(ABC): throttling of API requests through the usage of asynchronous context managers. """ + _default_config_map = {} _logger = None @classmethod @@ -40,7 +41,7 @@ def __init__(self, bots operate with the same account) """ # If configured, users can define the percentage of rate limits to allocate to the throttler. - share_percentage = limits_share_percentage or self._client_config_map().rate_limits_share_pct + share_percentage = limits_share_percentage or Decimal("100") self.limits_pct: Decimal = share_percentage / 100 self.set_rate_limits(rate_limits) diff --git a/hummingbot/core/event/events.py b/hummingbot/core/event/events.py index 551cc8406b..f53258ae65 100644 --- a/hummingbot/core/event/events.py +++ b/hummingbot/core/event/events.py @@ -37,6 +37,7 @@ class MarketEvent(Enum): class OrderBookEvent(int, Enum): TradeEvent = 901 + OrderBookDataSourceUpdateEvent = 904 class OrderBookDataSourceEvent(int, Enum): diff --git a/hummingbot/smart_components/position_executor/__init__.py b/hummingbot/core/rate_oracle/sources/__init__.py similarity index 100% rename from hummingbot/smart_components/position_executor/__init__.py rename to hummingbot/core/rate_oracle/sources/__init__.py diff --git a/hummingbot/core/utils/gateway_config_utils.py b/hummingbot/core/utils/gateway_config_utils.py index 8655c27985..612978fb97 100644 --- a/hummingbot/core/utils/gateway_config_utils.py +++ b/hummingbot/core/utils/gateway_config_utils.py @@ -16,6 +16,7 @@ "injective": "INJ", "xdc": "XDC", "tezos": "XTZ", + "kujira": "KUJI" } SUPPORTED_CHAINS = set(native_tokens.keys()) diff --git a/hummingbot/data_feed/candles_feed/ascend_ex_spot_candles/ascend_ex_spot_candles.py b/hummingbot/data_feed/candles_feed/ascend_ex_spot_candles/ascend_ex_spot_candles.py index ae2a179fcc..e4af3abed6 100644 --- a/hummingbot/data_feed/candles_feed/ascend_ex_spot_candles/ascend_ex_spot_candles.py +++ b/hummingbot/data_feed/candles_feed/ascend_ex_spot_candles/ascend_ex_spot_candles.py @@ -26,7 +26,7 @@ def __init__(self, trading_pair: str, interval: str = "1m", max_records: int = 1 @property def name(self): - return f"ascend_ex_spot_{self._trading_pair}" + return f"ascend_ex_{self._trading_pair}" @property def rest_url(self): diff --git a/hummingbot/data_feed/candles_feed/binance_perpetual_candles/binance_perpetual_candles.py b/hummingbot/data_feed/candles_feed/binance_perpetual_candles/binance_perpetual_candles.py index efd9826cb4..91f72b3c60 100644 --- a/hummingbot/data_feed/candles_feed/binance_perpetual_candles/binance_perpetual_candles.py +++ b/hummingbot/data_feed/candles_feed/binance_perpetual_candles/binance_perpetual_candles.py @@ -26,7 +26,7 @@ def __init__(self, trading_pair: str, interval: str = "1m", max_records: int = 1 @property def name(self): - return f"binance_perpetuals_{self._trading_pair}" + return f"binance_perpetual_{self._trading_pair}" @property def rest_url(self): diff --git a/hummingbot/data_feed/candles_feed/binance_spot_candles/binance_spot_candles.py b/hummingbot/data_feed/candles_feed/binance_spot_candles/binance_spot_candles.py index 3ef225af69..374d821c5e 100644 --- a/hummingbot/data_feed/candles_feed/binance_spot_candles/binance_spot_candles.py +++ b/hummingbot/data_feed/candles_feed/binance_spot_candles/binance_spot_candles.py @@ -26,7 +26,7 @@ def __init__(self, trading_pair: str, interval: str = "1m", max_records: int = 1 @property def name(self): - return f"binance_spot_{self._trading_pair}" + return f"binance_{self._trading_pair}" @property def rest_url(self): diff --git a/hummingbot/data_feed/candles_feed/candles_base.py b/hummingbot/data_feed/candles_feed/candles_base.py index 77b75560e6..8a79976f3b 100644 --- a/hummingbot/data_feed/candles_feed/candles_base.py +++ b/hummingbot/data_feed/candles_feed/candles_base.py @@ -1,4 +1,5 @@ import asyncio +import os from collections import deque from typing import Optional @@ -119,6 +120,19 @@ def candles_df(self) -> pd.DataFrame: def get_exchange_trading_pair(self, trading_pair): raise NotImplementedError + def load_candles_from_csv(self, data_path: str): + """ + This method loads the candles from a CSV file. + :param data_path: data path that holds the CSV file + """ + filename = f"candles_{self.name}_{self.interval}.csv" + file_path = os.path.join(data_path, filename) + if not os.path.exists(file_path): + raise FileNotFoundError(f"File '{file_path}' does not exist.") + df = pd.read_csv(file_path) + df.sort_values(by="timestamp", ascending=False, inplace=True) + self._candles.extendleft(df.values.tolist()) + async def fetch_candles(self, start_time: Optional[int] = None, end_time: Optional[int] = None, diff --git a/hummingbot/data_feed/candles_feed/candles_factory.py b/hummingbot/data_feed/candles_feed/candles_factory.py index 44ff73a770..4da0800e74 100644 --- a/hummingbot/data_feed/candles_feed/candles_factory.py +++ b/hummingbot/data_feed/candles_feed/candles_factory.py @@ -1,3 +1,5 @@ +from pydantic import BaseModel + from hummingbot.data_feed.candles_feed.ascend_ex_spot_candles.ascend_ex_spot_candles import AscendExSpotCandles from hummingbot.data_feed.candles_feed.binance_perpetual_candles import BinancePerpetualCandles from hummingbot.data_feed.candles_feed.binance_spot_candles import BinanceSpotCandles @@ -6,6 +8,21 @@ from hummingbot.data_feed.candles_feed.kucoin_spot_candles.kucoin_spot_candles import KucoinSpotCandles +class CandlesConfig(BaseModel): + """ + The CandlesConfig class is a data class that stores the configuration of a Candle object. + It has the following attributes: + - connector: str + - trading_pair: str + - interval: str + - max_records: int + """ + connector: str + trading_pair: str + interval: str = "1m" + max_records: int = 500 + + class CandlesFactory: """ The CandlesFactory class creates and returns a Candle object based on the specified connector and trading pair. @@ -14,7 +31,16 @@ class CandlesFactory: If an unsupported connector is provided, it raises an exception. """ @classmethod - def get_candle(cls, connector: str, trading_pair: str, interval: str = "1m", max_records: int = 500): + def get_candle(cls, candles_config: CandlesConfig): + """ + Returns a Candle object based on the specified connector and trading pair. + :param candles_config: CandlesConfig + :return: Candles + """ + connector = candles_config.connector + trading_pair = candles_config.trading_pair + interval = candles_config.interval + max_records = candles_config.max_records if connector == "binance_perpetual": return BinancePerpetualCandles(trading_pair, interval, max_records) elif connector == "binance": diff --git a/hummingbot/data_feed/candles_feed/gate_io_spot_candles/gate_io_spot_candles.py b/hummingbot/data_feed/candles_feed/gate_io_spot_candles/gate_io_spot_candles.py index 5be2fe2963..ceb2182921 100644 --- a/hummingbot/data_feed/candles_feed/gate_io_spot_candles/gate_io_spot_candles.py +++ b/hummingbot/data_feed/candles_feed/gate_io_spot_candles/gate_io_spot_candles.py @@ -27,7 +27,7 @@ def __init__(self, trading_pair: str, interval: str = "1m", max_records: int = 1 @property def name(self): - return f"gate_io_spot_{self._trading_pair}" + return f"gate_io_{self._trading_pair}" @property def rest_url(self): diff --git a/hummingbot/data_feed/candles_feed/kucoin_spot_candles/kucoin_spot_candles.py b/hummingbot/data_feed/candles_feed/kucoin_spot_candles/kucoin_spot_candles.py index 326f4c86d1..f726aaabe4 100644 --- a/hummingbot/data_feed/candles_feed/kucoin_spot_candles/kucoin_spot_candles.py +++ b/hummingbot/data_feed/candles_feed/kucoin_spot_candles/kucoin_spot_candles.py @@ -31,7 +31,7 @@ def __init__(self, trading_pair: str, interval: str = "1min", max_records: int = @property def name(self): - return f"kucoin_spot_{self._trading_pair}" + return f"kucoin_{self._trading_pair}" @property def rest_url(self): diff --git a/test/hummingbot/smart_components/arbitrage_executor/__init__.py b/hummingbot/smart_components/controllers/__init__.py similarity index 100% rename from test/hummingbot/smart_components/arbitrage_executor/__init__.py rename to hummingbot/smart_components/controllers/__init__.py diff --git a/hummingbot/smart_components/controllers/bollinger_grid.py b/hummingbot/smart_components/controllers/bollinger_grid.py new file mode 100644 index 0000000000..f3f2b478c3 --- /dev/null +++ b/hummingbot/smart_components/controllers/bollinger_grid.py @@ -0,0 +1,103 @@ +import time +from decimal import Decimal + +import pandas_ta as ta # noqa: F401 + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, +) + + +class BollingerGridConfig(MarketMakingControllerConfigBase): + strategy_name: str = "bollinger_grid" + bb_length: int = 12 + natr_length: int = 14 + + +class BollingerGrid(MarketMakingControllerBase): + """ + Directional Market Making Strategy making use of NATR indicator to make spreads dynamic and shift the mid price. + """ + + def __init__(self, config: BollingerGridConfig): + super().__init__(config) + self.config = config + + def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + Checks if the order needs to be refreshed. + You can reimplement this method to add more conditions. + """ + if executor.position_config.timestamp + order_level.order_refresh_time > time.time(): + return False + return True + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + If an executor has an active position, should we close it based on a condition. + """ + return False + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + After finishing an order, the executor will be in cooldown for a certain amount of time. + This prevents the executor from creating a new order immediately after finishing one and execute a lot + of orders in a short period of time from the same side. + """ + if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time(): + return True + return False + + def get_processed_data(self): + """ + Gets the price and spread multiplier from the last candlestick. + """ + candles_df = self.candles[0].candles_df + natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100 + candles_df.ta.bbands(length=self.config.bb_length, std=2, append=True) + bbp = candles_df[f"BBP_{self.config.bb_length}_2.0"] + + candles_df["spread_multiplier"] = natr + candles_df["price_multiplier"] = bbp + return candles_df + + def get_position_config(self, order_level: OrderLevel) -> PositionConfig: + """ + Creates a PositionConfig object from an OrderLevel object. + Here you can use technical indicators to determine the parameters of the position config. + """ + close_price = self.get_close_price(self.config.exchange, self.config.trading_pair) + bbp, spread_multiplier = self.get_price_and_spread_multiplier() + side_multiplier = -1 if order_level.side == TradeType.BUY else 1 + + if (bbp > 0.7 and side_multiplier == 1) or (bbp < 0.3 and side_multiplier == -1): + order_price = close_price * (1 + order_level.spread_factor * spread_multiplier * side_multiplier) + amount = order_level.order_amount_usd / order_price + if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta: + trailing_stop = TrailingStop( + activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta, + trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta, + ) + else: + trailing_stop = None + position_config = PositionConfig( + timestamp=time.time(), + trading_pair=self.config.trading_pair, + exchange=self.config.exchange, + side=order_level.side, + amount=amount, + take_profit=order_level.triple_barrier_conf.take_profit, + stop_loss=order_level.triple_barrier_conf.stop_loss, + time_limit=order_level.triple_barrier_conf.time_limit, + entry_price=Decimal(order_price), + open_order_type=order_level.triple_barrier_conf.open_order_type, + take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type, + trailing_stop=trailing_stop, + leverage=self.config.leverage + ) + return position_config diff --git a/hummingbot/smart_components/controllers/dman_v1.py b/hummingbot/smart_components/controllers/dman_v1.py new file mode 100644 index 0000000000..6e1ce3fbf0 --- /dev/null +++ b/hummingbot/smart_components/controllers/dman_v1.py @@ -0,0 +1,100 @@ +import time +from decimal import Decimal + +import pandas_ta as ta # noqa: F401 + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, +) + + +class DManV1Config(MarketMakingControllerConfigBase): + strategy_name: str = "dman_v1" + natr_length: int = 14 + + +class DManV1(MarketMakingControllerBase): + """ + Directional Market Making Strategy making use of NATR indicator to make spreads dynamic. + """ + + def __init__(self, config: DManV1Config): + super().__init__(config) + self.config = config + + def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + Checks if the order needs to be refreshed. + You can reimplement this method to add more conditions. + """ + if executor.position_config.timestamp + order_level.order_refresh_time > time.time(): + return False + return True + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + If an executor has an active position, should we close it based on a condition. + """ + return False + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + After finishing an order, the executor will be in cooldown for a certain amount of time. + This prevents the executor from creating a new order immediately after finishing one and execute a lot + of orders in a short period of time from the same side. + """ + if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time(): + return True + return False + + def get_processed_data(self): + """ + Gets the price and spread multiplier from the last candlestick. + """ + candles_df = self.candles[0].candles_df + natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100 + + candles_df["spread_multiplier"] = natr + candles_df["price_multiplier"] = 0.0 + return candles_df + + def get_position_config(self, order_level: OrderLevel) -> PositionConfig: + """ + Creates a PositionConfig object from an OrderLevel object. + Here you can use technical indicators to determine the parameters of the position config. + """ + close_price = self.get_close_price(self.config.exchange, self.config.trading_pair) + amount = order_level.order_amount_usd / close_price + price_multiplier, spread_multiplier = self.get_price_and_spread_multiplier() + + price_adjusted = close_price * (1 + price_multiplier) + side_multiplier = -1 if order_level.side == TradeType.BUY else 1 + order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier) + if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta: + trailing_stop = TrailingStop( + activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta, + trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta, + ) + else: + trailing_stop = None + position_config = PositionConfig( + timestamp=time.time(), + trading_pair=self.config.trading_pair, + exchange=self.config.exchange, + side=order_level.side, + amount=amount, + take_profit=order_level.triple_barrier_conf.take_profit, + stop_loss=order_level.triple_barrier_conf.stop_loss, + time_limit=order_level.triple_barrier_conf.time_limit, + entry_price=Decimal(order_price), + open_order_type=order_level.triple_barrier_conf.open_order_type, + take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type, + trailing_stop=trailing_stop, + leverage=self.config.leverage + ) + return position_config diff --git a/hummingbot/smart_components/controllers/dman_v2.py b/hummingbot/smart_components/controllers/dman_v2.py new file mode 100644 index 0000000000..e220352b7a --- /dev/null +++ b/hummingbot/smart_components/controllers/dman_v2.py @@ -0,0 +1,112 @@ +import time +from decimal import Decimal + +import pandas_ta as ta # noqa: F401 + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, +) + + +class DManV2Config(MarketMakingControllerConfigBase): + strategy_name: str = "dman_v2" + macd_fast: int = 12 + macd_slow: int = 26 + macd_signal: int = 9 + natr_length: int = 14 + + +class DManV2(MarketMakingControllerBase): + """ + Directional Market Making Strategy making use of NATR indicator to make spreads dynamic and shift the mid price. + """ + + def __init__(self, config: DManV2Config): + super().__init__(config) + self.config = config + + def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + Checks if the order needs to be refreshed. + You can reimplement this method to add more conditions. + """ + if executor.position_config.timestamp + order_level.order_refresh_time > time.time(): + return False + return True + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + If an executor has an active position, should we close it based on a condition. + """ + return False + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + After finishing an order, the executor will be in cooldown for a certain amount of time. + This prevents the executor from creating a new order immediately after finishing one and execute a lot + of orders in a short period of time from the same side. + """ + if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time(): + return True + return False + + def get_processed_data(self): + """ + Gets the price and spread multiplier from the last candlestick. + """ + candles_df = self.candles[0].candles_df + natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100 + + macd_output = ta.macd(candles_df["close"], fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal) + macd = macd_output[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"] + macdh = macd_output[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"] + macd_signal = - (macd - macd.mean()) / macd.std() + macdh_signal = macdh.apply(lambda x: 1 if x > 0 else -1) + max_price_shift = natr / 2 + + price_multiplier = (0.5 * macd_signal + 0.5 * macdh_signal) * max_price_shift + + candles_df["spread_multiplier"] = natr + candles_df["price_multiplier"] = price_multiplier + return candles_df + + def get_position_config(self, order_level: OrderLevel) -> PositionConfig: + """ + Creates a PositionConfig object from an OrderLevel object. + Here you can use technical indicators to determine the parameters of the position config. + """ + close_price = self.get_close_price(self.config.exchange, self.config.trading_pair) + amount = order_level.order_amount_usd / close_price + price_multiplier, spread_multiplier = self.get_price_and_spread_multiplier() + + price_adjusted = close_price * (1 + price_multiplier) + side_multiplier = -1 if order_level.side == TradeType.BUY else 1 + order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier) + if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta: + trailing_stop = TrailingStop( + activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta, + trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta, + ) + else: + trailing_stop = None + position_config = PositionConfig( + timestamp=time.time(), + trading_pair=self.config.trading_pair, + exchange=self.config.exchange, + side=order_level.side, + amount=amount, + take_profit=order_level.triple_barrier_conf.take_profit, + stop_loss=order_level.triple_barrier_conf.stop_loss, + time_limit=order_level.triple_barrier_conf.time_limit, + entry_price=Decimal(order_price), + open_order_type=order_level.triple_barrier_conf.open_order_type, + take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type, + trailing_stop=trailing_stop, + leverage=self.config.leverage + ) + return position_config diff --git a/hummingbot/smart_components/controllers/macd_bb_v1.py b/hummingbot/smart_components/controllers/macd_bb_v1.py new file mode 100644 index 0000000000..65045772c3 --- /dev/null +++ b/hummingbot/smart_components/controllers/macd_bb_v1.py @@ -0,0 +1,77 @@ +import time +from typing import Optional + +import pandas as pd +from pydantic import Field + +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel +from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import ( + DirectionalTradingControllerBase, + DirectionalTradingControllerConfigBase, +) + + +class MACDBBV1Config(DirectionalTradingControllerConfigBase): + strategy_name: str = "dman_v1" + bb_length: int = Field(default=24, ge=2, le=1000) + bb_std: float = Field(default=2.0, ge=0.5, le=4.0) + bb_long_threshold: float = Field(default=0.0, ge=-3.0, le=0.5) + bb_short_threshold: float = Field(default=1.0, ge=0.5, le=3.0) + macd_fast: int = Field(default=21, ge=2, le=100) + macd_slow: int = Field(default=42, ge=30, le=1000) + macd_signal: int = Field(default=9, ge=2, le=100) + std_span: Optional[int] = None + + +class MACDBBV1(DirectionalTradingControllerBase): + """ + Directional Market Making Strategy making use of NATR indicator to make spreads dynamic. + """ + + def __init__(self, config: MACDBBV1Config): + super().__init__(config) + self.config = config + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + If an executor has an active position, should we close it based on a condition. + """ + return False + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + """ + After finishing an order, the executor will be in cooldown for a certain amount of time. + This prevents the executor from creating a new order immediately after finishing one and execute a lot + of orders in a short period of time from the same side. + """ + if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time(): + return True + return False + + def get_processed_data(self) -> pd.DataFrame: + df = self.candles[0].candles_df + + # Add indicators + df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True) + df.ta.macd(fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal, append=True) + bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"] + macdh = df[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"] + macd = df[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"] + + # Generate signal + long_condition = (bbp < self.config.bb_long_threshold) & (macdh > 0) & (macd < 0) + short_condition = (bbp > self.config.bb_short_threshold) & (macdh < 0) & (macd > 0) + df["signal"] = 0 + df.loc[long_condition, "signal"] = 1 + df.loc[short_condition, "signal"] = -1 + + # Optional: Generate spread multiplier + if self.config.std_span: + df["target"] = df["close"].rolling(self.config.std_span).std() / df["close"] + return df + + def extra_columns_to_show(self): + return [f"BBP_{self.config.bb_length}_{self.config.bb_std}", + f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}", + f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"] diff --git a/test/hummingbot/smart_components/position_executor/__init__.py b/hummingbot/smart_components/executors/__init__.py similarity index 100% rename from test/hummingbot/smart_components/position_executor/__init__.py rename to hummingbot/smart_components/executors/__init__.py diff --git a/hummingbot/smart_components/executors/arbitrage_executor/__init__.py b/hummingbot/smart_components/executors/arbitrage_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/smart_components/arbitrage_executor/arbitrage_executor.py b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py similarity index 98% rename from hummingbot/smart_components/arbitrage_executor/arbitrage_executor.py rename to hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py index 36270ba24b..1da2a2266d 100644 --- a/hummingbot/smart_components/arbitrage_executor/arbitrage_executor.py +++ b/hummingbot/smart_components/executors/arbitrage_executor/arbitrage_executor.py @@ -10,8 +10,8 @@ from hummingbot.core.event.events import BuyOrderCreatedEvent, MarketOrderFailureEvent, SellOrderCreatedEvent from hummingbot.core.rate_oracle.rate_oracle import RateOracle from hummingbot.logger import HummingbotLogger -from hummingbot.smart_components.arbitrage_executor.data_types import ArbitrageConfig, ArbitrageExecutorStatus -from hummingbot.smart_components.position_executor.data_types import TrackedOrder +from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageConfig, ArbitrageExecutorStatus +from hummingbot.smart_components.executors.position_executor.data_types import TrackedOrder from hummingbot.smart_components.smart_component_base import SmartComponentBase from hummingbot.strategy.script_strategy_base import ScriptStrategyBase diff --git a/hummingbot/smart_components/arbitrage_executor/data_types.py b/hummingbot/smart_components/executors/arbitrage_executor/data_types.py similarity index 100% rename from hummingbot/smart_components/arbitrage_executor/data_types.py rename to hummingbot/smart_components/executors/arbitrage_executor/data_types.py diff --git a/hummingbot/smart_components/executors/position_executor/__init__.py b/hummingbot/smart_components/executors/position_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/smart_components/position_executor/data_types.py b/hummingbot/smart_components/executors/position_executor/data_types.py similarity index 100% rename from hummingbot/smart_components/position_executor/data_types.py rename to hummingbot/smart_components/executors/position_executor/data_types.py diff --git a/hummingbot/smart_components/position_executor/position_executor.py b/hummingbot/smart_components/executors/position_executor/position_executor.py similarity index 92% rename from hummingbot/smart_components/position_executor/position_executor.py rename to hummingbot/smart_components/executors/position_executor/position_executor.py index 4c043292b0..f4ddfc8a6f 100644 --- a/hummingbot/smart_components/position_executor/position_executor.py +++ b/hummingbot/smart_components/executors/position_executor/position_executor.py @@ -1,4 +1,5 @@ import logging +import math from decimal import Decimal from typing import Union @@ -14,7 +15,7 @@ SellOrderCreatedEvent, ) from hummingbot.logger import HummingbotLogger -from hummingbot.smart_components.position_executor.data_types import ( +from hummingbot.smart_components.executors.position_executor.data_types import ( CloseType, PositionConfig, PositionExecutorStatus, @@ -287,7 +288,7 @@ def control_take_profit(self): if self.take_profit_order_type.is_limit_type(): if not self.take_profit_order.order_id: self.place_take_profit_limit_order() - elif self.take_profit_order.executed_amount_base != self.open_order.executed_amount_base: + elif not math.isclose(self.take_profit_order.order.amount, self.open_order.executed_amount_base): self.renew_take_profit_order() elif self.take_profit_condition(): self.place_close_order(close_type=CloseType.TAKE_PROFIT) @@ -356,6 +357,8 @@ def process_order_completed_event(self, _, market, event: Union[BuyOrderComplete self.close_type = CloseType.TAKE_PROFIT self.executor_status = PositionExecutorStatus.COMPLETED self.close_timestamp = event.timestamp + self.close_order.order_id = event.order_id + self.close_order.order = self.take_profit_order.order self.logger().info(f"Closed by {self.close_type}") self.terminate_control_loop() @@ -380,6 +383,33 @@ def process_order_failed_event(self, _, market, event: MarketOrderFailureEvent): elif self.take_profit_order.order_id == event.order_id: self.place_take_profit_limit_order() + def to_json(self): + return { + "timestamp": self.position_config.timestamp, + "exchange": self.exchange, + "trading_pair": self.trading_pair, + "side": self.side.name, + "amount": self.amount, + "trade_pnl": self.trade_pnl, + "trade_pnl_quote": self.trade_pnl_quote, + "cum_fee_quote": self.cum_fee_quote, + "net_pnl_quote": self.net_pnl_quote, + "net_pnl": self.net_pnl, + "close_timestamp": self.close_timestamp, + "executor_status": self.executor_status, + "close_type": self.close_type.name if self.close_type else None, + "entry_price": self.entry_price, + "close_price": self.close_price, + "sl": self.position_config.stop_loss, + "tp": self.position_config.take_profit, + "tl": self.position_config.time_limit, + "open_order_type": self.open_order_type, + "take_profit_order_type": self.take_profit_order_type, + "stop_loss_order_type": self.stop_loss_order_type, + "time_limit_order_type": self.time_limit_order_type, + "leverage": self.position_config.leverage, + } + def to_format_status(self, scale=1.0): lines = [] current_price = self.get_price(self.exchange, self.trading_pair) @@ -469,7 +499,7 @@ def check_budget(self): order_side=self.side, amount=self.amount, price=self.entry_price, - leverage=self.position_config.leverage, + leverage=Decimal(self.position_config.leverage), ) else: order_candidate = OrderCandidate( diff --git a/hummingbot/smart_components/strategy_frameworks/__init__.py b/hummingbot/smart_components/strategy_frameworks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/smart_components/strategy_frameworks/backtesting_engine_base.py b/hummingbot/smart_components/strategy_frameworks/backtesting_engine_base.py new file mode 100644 index 0000000000..854b3860c6 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/backtesting_engine_base.py @@ -0,0 +1,198 @@ +from datetime import datetime +from typing import Optional + +import numpy as np +import pandas as pd + +from hummingbot import data_path +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase + + +class BacktestingEngineBase: + def __init__(self, controller: ControllerBase): + """ + Initialize the BacktestExecutorBase. + + :param controller: The controller instance. + :param start_date: Start date for backtesting. + :param end_date: End date for backtesting. + """ + self.controller = controller + self.level_executors = {level.level_id: pd.Timestamp.min for level in self.controller.config.order_levels} + self.processed_data = None + self.executors_df = None + self.results = None + + @staticmethod + def filter_df_by_time(df, start: Optional[str] = None, end: Optional[str] = None): + if start is not None: + start_condition = pd.to_datetime(df["timestamp"], unit="ms") >= datetime.strptime(start, "%Y-%m-%d") + else: + start_condition = pd.Series([True] * len(df)) + if end is not None: + end_condition = pd.to_datetime(df["timestamp"], unit="ms") <= datetime.strptime(end, "%Y-%m-%d") + else: + end_condition = pd.Series([True] * len(df)) + return df[start_condition & end_condition] + + def apply_triple_barrier_method(self, df, tp=1.0, sl=1.0, tl=5, trade_cost=0.0006): + df.index = pd.to_datetime(df.timestamp, unit="ms") + if "target" not in df.columns: + df["target"] = 1 + df["tl"] = df.index + pd.Timedelta(seconds=tl) + df.dropna(subset="target", inplace=True) + + df = self.apply_tp_sl_on_tl(df, tp=tp, sl=sl) + + df = self.get_bins(df, trade_cost) + df["tp"] = df["target"] * tp + df["sl"] = df["target"] * sl + + df["take_profit_price"] = df["close"] * (1 + df["tp"] * df["signal"]) + df["stop_loss_price"] = df["close"] * (1 - df["sl"] * df["signal"]) + + return df + + @staticmethod + def get_bins(df, trade_cost): + # 1) prices aligned with events + px = df.index.union(df["tl"].values).drop_duplicates() + px = df.close.reindex(px, method="ffill") + + # 2) create out object + df["trade_pnl"] = (px.loc[df["close_time"].values].values / px.loc[df.index] - 1) * df["signal"] + df["net_pnl"] = df["trade_pnl"] - trade_cost + df["profitable"] = np.sign(df["trade_pnl"] - trade_cost) + df["close_price"] = px.loc[df["close_time"].values].values + return df + + @staticmethod + def apply_tp_sl_on_tl(df: pd.DataFrame, tp: float, sl: float): + events = df[df["signal"] != 0].copy() + if tp > 0: + take_profit = tp * events["target"] + else: + take_profit = pd.Series(index=df.index) # NaNs + if sl > 0: + stop_loss = - sl * events["target"] + else: + stop_loss = pd.Series(index=df.index) # NaNs + + for loc, tl in events["tl"].fillna(df.index[-1]).items(): + df0 = df.close[loc:tl] # path prices + df0 = (df0 / df.close[loc] - 1) * events.at[loc, "signal"] # path returns + df.loc[loc, "stop_loss_time"] = df0[df0 < stop_loss[loc]].index.min() # earliest stop loss. + df.loc[loc, "take_profit_time"] = df0[df0 > take_profit[loc]].index.min() # earliest profit taking. + df["close_time"] = df[["tl", "take_profit_time", "stop_loss_time"]].dropna(how="all").min(axis=1) + df["close_type"] = df[["take_profit_time", "stop_loss_time", "tl"]].dropna(how="all").idxmin(axis=1) + df["close_type"].replace({"take_profit_time": "tp", "stop_loss_time": "sl"}, inplace=True) + return df + + def load_controller_data(self, data_path: str = data_path()): + self.controller.load_historical_data(data_path=data_path) + + def get_data(self, start: Optional[str] = None, end: Optional[str] = None): + df = self.controller.get_processed_data() + return self.filter_df_by_time(df, start, end).copy() + + def run_backtesting(self, initial_portfolio_usd=1000, trade_cost=0.0006, + start: Optional[str] = None, end: Optional[str] = None): + # Load historical candles + processed_data = self.get_data(start=start, end=end) + + # Apply the specific execution logic of the executor handler vectorized + executors_df = self.simulate_execution(processed_data, initial_portfolio_usd=initial_portfolio_usd, trade_cost=trade_cost) + + # Store data for further analysis + self.processed_data = processed_data + self.executors_df = executors_df + self.results = self.summarize_results(executors_df) + return { + "processed_data": processed_data, + "executors_df": executors_df, + "results": self.results + } + + def simulate_execution(self, df: pd.DataFrame, initial_portfolio_usd: float, trade_cost: float): + raise NotImplementedError + + @staticmethod + def summarize_results(executors_df): + if len(executors_df) > 0: + net_pnl = executors_df["net_pnl"].sum() + net_pnl_quote = executors_df["net_pnl_quote"].sum() + total_executors = executors_df.shape[0] + executors_with_position = executors_df[executors_df["net_pnl"] != 0] + total_executors_with_position = executors_with_position.shape[0] + total_volume = executors_with_position["amount"].sum() * 2 + total_long = (executors_with_position["side"] == "BUY").sum() + total_short = (executors_with_position["side"] == "SELL").sum() + correct_long = ((executors_with_position["side"] == "BUY") & (executors_with_position["net_pnl"] > 0)).sum() + correct_short = ((executors_with_position["side"] == "SELL") & (executors_with_position["net_pnl"] > 0)).sum() + accuracy_long = correct_long / total_long if total_long > 0 else 0 + accuracy_short = correct_short / total_short if total_short > 0 else 0 + close_types = executors_df.groupby("close_type")["timestamp"].count() + + # Additional metrics + total_positions = executors_df.shape[0] + win_signals = executors_df.loc[(executors_df["profitable"] > 0) & (executors_df["signal"] != 0)] + loss_signals = executors_df.loc[(executors_df["profitable"] < 0) & (executors_df["signal"] != 0)] + accuracy = win_signals.shape[0] / total_positions + cumulative_returns = executors_df["net_pnl_quote"].cumsum() + peak = np.maximum.accumulate(cumulative_returns) + drawdown = (cumulative_returns - peak) + max_draw_down = np.min(drawdown) + max_drawdown_pct = max_draw_down / executors_df["inventory"].iloc[0] + returns = executors_df["net_pnl_quote"] / net_pnl + sharpe_ratio = returns.mean() / returns.std() + total_won = win_signals.loc[:, "net_pnl_quote"].sum() + total_loss = - loss_signals.loc[:, "net_pnl_quote"].sum() + profit_factor = total_won / total_loss if total_loss > 0 else 1 + duration_minutes = (executors_df.close_time.max() - executors_df.index.min()).total_seconds() / 60 + avg_trading_time_minutes = (pd.to_datetime(executors_df["close_time"]) - executors_df.index).dt.total_seconds() / 60 + avg_trading_time = avg_trading_time_minutes.mean() + + return { + "net_pnl": net_pnl, + "net_pnl_quote": net_pnl_quote, + "total_executors": total_executors, + "total_executors_with_position": total_executors_with_position, + "total_volume": total_volume, + "total_long": total_long, + "total_short": total_short, + "close_types": close_types, + "accuracy_long": accuracy_long, + "accuracy_short": accuracy_short, + "total_positions": total_positions, + "accuracy": accuracy, + "max_drawdown_usd": max_draw_down, + "max_drawdown_pct": max_drawdown_pct, + "sharpe_ratio": sharpe_ratio, + "profit_factor": profit_factor, + "duration_minutes": duration_minutes, + "avg_trading_time_minutes": avg_trading_time, + "win_signals": win_signals.shape[0], + "loss_signals": loss_signals.shape[0], + } + return { + "net_pnl": 0, + "net_pnl_quote": 0, + "total_executors": 0, + "total_executors_with_position": 0, + "total_volume": 0, + "total_long": 0, + "total_short": 0, + "close_types": 0, + "accuracy_long": 0, + "accuracy_short": 0, + "total_positions": 0, + "accuracy": 0, + "max_drawdown_usd": 0, + "max_drawdown_pct": 0, + "sharpe_ratio": 0, + "profit_factor": 0, + "duration_minutes": 0, + "avg_trading_time_minutes": 0, + "win_signals": 0, + "loss_signals": 0, + } diff --git a/hummingbot/smart_components/strategy_frameworks/controller_base.py b/hummingbot/smart_components/strategy_frameworks/controller_base.py new file mode 100644 index 0000000000..d1528b846a --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/controller_base.py @@ -0,0 +1,123 @@ +from abc import ABC +from decimal import Decimal +from typing import List, Optional + +from pydantic import BaseModel + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel + + +class ControllerConfigBase(BaseModel): + strategy_name: str + candles_config: List[CandlesConfig] + order_levels: List[OrderLevel] + + +class ControllerBase(ABC): + """ + Abstract base class for controllers. + """ + + def __init__(self, + config: ControllerConfigBase, + excluded_parameters: Optional[List[str]] = None): + """ + Initialize the ControllerBase. + + :param config: Configuration for the controller. + :param mode: Mode of the controller (LIVE or other modes). + :param excluded_parameters: List of parameters to exclude from status formatting. + """ + self.config = config + self._excluded_parameters = excluded_parameters or ["order_levels", "candles_config"] + self.candles = self.initialize_candles(config.candles_config) + + def get_processed_data(self): + """ + Get the processed data. + """ + pass + + def filter_executors_df(self, df): + """ + In case that you are running the multiple controllers in the same script, you should implement this method + to recognize the executors that belongs to this controller. + """ + return df + + def initialize_candles(self, candles_config: List[CandlesConfig]): + return [CandlesFactory.get_candle(candles_config) for candles_config in candles_config] + + def get_close_price(self, connector: str, trading_pair: str): + """ + Gets the close price of the last candlestick. + """ + candles = self.get_candles_by_connector_trading_pair(connector, trading_pair) + first_candle = list(candles.values())[0] + return Decimal(first_candle.candles_df["close"].iloc[-1]) + + def get_candles_by_connector_trading_pair(self, connector: str, trading_pair: str): + """ + Gets all the candlesticks with the given connector and trading pair. + """ + candle_name = f"{connector}_{trading_pair}" + return self.get_candles_dict()[candle_name] + + def get_candle(self, connector: str, trading_pair: str, interval: str): + """ + Gets the candlestick with the given connector, trading pair and interval. + """ + return self.get_candles_by_connector_trading_pair(connector, trading_pair)[interval] + + def get_candles_dict(self) -> dict: + candles = {candle.name: {} for candle in self.candles} + for candle in self.candles: + candles[candle.name][candle.interval] = candle + return candles + + @property + def all_candles_ready(self): + """ + Checks if the candlesticks are full. + """ + return all([candle.is_ready for candle in self.candles]) + + def start(self) -> None: + """ + Start the controller. + """ + for candle in self.candles: + candle.start() + + def load_historical_data(self, data_path: str): + for candle in self.candles: + candle.load_candles_from_csv(data_path) + + def stop(self) -> None: + """ + Stop the controller. + """ + for candle in self.candles: + candle.stop() + + def get_csv_prefix(self) -> str: + """ + Get the CSV prefix based on the strategy name. + + :return: CSV prefix string. + """ + return f"{self.config.strategy_name}" + + def to_format_status(self) -> list: + """ + Format and return the status of the controller. + + :return: Formatted status string. + """ + lines = [] + lines.extend(["\n################################ Controller Config ################################"]) + for parameter, value in self.config.dict().items(): + if parameter not in self._excluded_parameters: + lines.extend([f" {parameter}: {value}"]) + return lines diff --git a/hummingbot/smart_components/strategy_frameworks/data_types.py b/hummingbot/smart_components/strategy_frameworks/data_types.py new file mode 100644 index 0000000000..94248f0634 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/data_types.py @@ -0,0 +1,50 @@ +from decimal import Decimal +from enum import Enum +from typing import Optional + +from pydantic import BaseModel, validator + +from hummingbot.core.data_type.common import OrderType, TradeType + + +class ExecutorHandlerStatus(Enum): + NOT_STARTED = 1 + ACTIVE = 2 + TERMINATED = 3 + + +class TripleBarrierConf(BaseModel): + # Configure the parameters for the position + stop_loss: Optional[Decimal] + take_profit: Optional[Decimal] + time_limit: Optional[int] + trailing_stop_activation_price_delta: Optional[Decimal] + trailing_stop_trailing_delta: Optional[Decimal] + # Configure the parameters for the order + open_order_type: OrderType = OrderType.LIMIT + take_profit_order_type: OrderType = OrderType.MARKET + stop_loss_order_type: OrderType = OrderType.MARKET + time_limit_order_type: OrderType = OrderType.MARKET + + @validator("stop_loss", "take_profit", "trailing_stop_activation_price_delta", "trailing_stop_trailing_delta", + pre=True) + def float_to_decimal(cls, v): + return Decimal(v) + + +class OrderLevel(BaseModel): + level: int + side: TradeType + order_amount_usd: Decimal + spread_factor: Decimal = Decimal("0.0") + order_refresh_time: int = 60 + cooldown_time: int = 0 + triple_barrier_conf: TripleBarrierConf + + @property + def level_id(self): + return f"{self.side.name}_{self.level}" + + @validator("order_amount_usd", "spread_factor", pre=True) + def float_to_decimal(cls, v): + return Decimal(v) diff --git a/hummingbot/smart_components/strategy_frameworks/directional_trading/__init__.py b/hummingbot/smart_components/strategy_frameworks/directional_trading/__init__.py new file mode 100644 index 0000000000..07af70f393 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/directional_trading/__init__.py @@ -0,0 +1,13 @@ +from .directional_trading_backtesting_engine import DirectionalTradingBacktestingEngine +from .directional_trading_controller_base import ( + DirectionalTradingControllerBase, + DirectionalTradingControllerConfigBase, +) +from .directional_trading_executor_handler import DirectionalTradingExecutorHandler + +__all__ = [ + "DirectionalTradingControllerConfigBase", + "DirectionalTradingControllerBase", + "DirectionalTradingBacktestingEngine", + "DirectionalTradingExecutorHandler" +] diff --git a/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_backtesting_engine.py b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_backtesting_engine.py new file mode 100644 index 0000000000..facfd019a5 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_backtesting_engine.py @@ -0,0 +1,28 @@ +import pandas as pd + +from hummingbot.smart_components.strategy_frameworks.backtesting_engine_base import BacktestingEngineBase + + +class DirectionalTradingBacktestingEngine(BacktestingEngineBase): + def simulate_execution(self, df, initial_portfolio_usd, trade_cost): + executors = [] + df["side"] = df["signal"].apply(lambda x: "BUY" if x > 0 else "SELL" if x < 0 else 0) + for order_level in self.controller.config.order_levels: + df = self.apply_triple_barrier_method(df, + tp=float(order_level.triple_barrier_conf.take_profit), + sl=float(order_level.triple_barrier_conf.stop_loss), + tl=int(order_level.triple_barrier_conf.time_limit), + trade_cost=trade_cost) + for index, row in df[(df["side"] == order_level.side.name)].iterrows(): + last_close_time = self.level_executors[order_level.level_id] + if index >= last_close_time + pd.Timedelta(seconds=order_level.cooldown_time): + row["order_level"] = order_level.level_id + row["amount"] = float(order_level.order_amount_usd) + row["net_pnl_quote"] = row["net_pnl"] * row["amount"] + executors.append(row) + self.level_executors[order_level.level_id] = row["close_time"] + executors_df = pd.DataFrame(executors).sort_index() + executors_df["inventory"] = initial_portfolio_usd + if len(executors_df) > 0: + executors_df["inventory"] = initial_portfolio_usd + executors_df["net_pnl_quote"].cumsum().shift().fillna(0) + return executors_df diff --git a/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_controller_base.py b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_controller_base.py new file mode 100644 index 0000000000..e7a9e2e1ae --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_controller_base.py @@ -0,0 +1,118 @@ +import time +from decimal import Decimal +from typing import List, Optional, Set + +import pandas as pd +from pydantic import Field + +from hummingbot.client.ui.interface_utils import format_df_for_printout +from hummingbot.core.data_type.common import PositionMode, TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase, ControllerConfigBase +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel + + +class DirectionalTradingControllerConfigBase(ControllerConfigBase): + exchange: str = Field(default="binance_perpetual") + trading_pair: str = Field(default="BTC-USDT") + leverage: int = Field(10, ge=1) + position_mode: PositionMode = Field(PositionMode.HEDGE) + + +class DirectionalTradingControllerBase(ControllerBase): + + def __init__(self, + config: DirectionalTradingControllerConfigBase, + excluded_parameters: Optional[List[str]] = None): + super().__init__(config, excluded_parameters) + self.config = config # this is only for type hints + + def filter_executors_df(self, df): + return df[df["trading_pair"] == self.config.trading_pair] + + def update_strategy_markets_dict(self, markets_dict: dict[str, Set] = {}): + if self.config.exchange not in markets_dict: + markets_dict[self.config.exchange] = {self.config.trading_pair} + else: + markets_dict[self.config.exchange].add(self.config.trading_pair) + return markets_dict + + @property + def is_perpetual(self): + """ + Checks if the exchange is a perpetual market. + """ + # TODO: Refactor this as a method of the base class that receives the exchange name as a parameter + return "perpetual" in self.config.exchange + + def get_signal(self): + df = self.get_processed_data() + return df["signal"].iloc[-1] + + def get_spread_multiplier(self): + df = self.get_processed_data() + if "target" in df.columns: + return Decimal(df["target"].iloc[-1]) + else: + return Decimal("1.0") + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + raise NotImplementedError + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + raise NotImplementedError + + def get_position_config(self, order_level: OrderLevel, signal: int) -> PositionConfig: + """ + Creates a PositionConfig object from an OrderLevel object. + Here you can use technical indicators to determine the parameters of the position config. + """ + if (signal == 1 and order_level.side == TradeType.BUY) or (signal == -1 and order_level.side == TradeType.SELL): + # Here you can use the weight of the signal to tweak for example the order amount + close_price = self.get_close_price(self.config.exchange, self.config.trading_pair) + amount = order_level.order_amount_usd / close_price + spread_multiplier = self.get_spread_multiplier() + order_price = close_price * (1 + order_level.spread_factor * spread_multiplier * signal) + if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta: + trailing_stop = TrailingStop( + activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta, + trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta, + ) + else: + trailing_stop = None + position_config = PositionConfig( + timestamp=time.time(), + trading_pair=self.config.trading_pair, + exchange=self.config.exchange, + side=order_level.side, + amount=amount, + take_profit=order_level.triple_barrier_conf.take_profit, + stop_loss=order_level.triple_barrier_conf.stop_loss, + time_limit=order_level.triple_barrier_conf.time_limit, + entry_price=Decimal(order_price), + open_order_type=order_level.triple_barrier_conf.open_order_type, + take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type, + trailing_stop=trailing_stop, + leverage=self.config.leverage + ) + return position_config + + def get_processed_data(self) -> pd.DataFrame: + """ + Retrieves the processed dataframe with indicators, signal, weight and spreads multipliers. + Returns: + pd.DataFrame: The processed dataframe with indicators, signal, weight and spreads multipliers. + """ + raise NotImplementedError + + def to_format_status(self) -> list: + lines = super().to_format_status() + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "signal"] + self.extra_columns_to_show() + df = self.get_processed_data() + prices_str = format_df_for_printout(df[columns_to_show].tail(4), table_format="psql") + lines.extend([f"{prices_str}"]) + return lines + + def extra_columns_to_show(self): + return [] diff --git a/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_executor_handler.py b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_executor_handler.py new file mode 100644 index 0000000000..bf5d208f5a --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/directional_trading/directional_trading_executor_handler.py @@ -0,0 +1,45 @@ +from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorStatus +from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import ( + DirectionalTradingControllerBase, +) +from hummingbot.smart_components.strategy_frameworks.executor_handler_base import ExecutorHandlerBase +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class DirectionalTradingExecutorHandler(ExecutorHandlerBase): + def __init__(self, strategy: ScriptStrategyBase, controller: DirectionalTradingControllerBase, + update_interval: float = 1.0): + super().__init__(strategy, controller, update_interval) + self.controller = controller + + def on_stop(self): + if self.controller.is_perpetual: + self.close_open_positions(connector_name=self.controller.config.exchange, trading_pair=self.controller.config.trading_pair) + super().on_stop() + + def on_start(self): + if self.controller.is_perpetual: + self.set_leverage_and_position_mode() + + def set_leverage_and_position_mode(self): + connector = self.strategy.connectors[self.controller.config.exchange] + connector.set_position_mode(self.controller.config.position_mode) + connector.set_leverage(trading_pair=self.controller.config.trading_pair, leverage=self.controller.config.leverage) + + async def control_task(self): + if self.controller.all_candles_ready: + signal = self.controller.get_signal() + if signal != 0: + for order_level in self.controller.config.order_levels: + current_executor = self.level_executors[order_level.level_id] + if current_executor: + closed_and_not_in_cooldown = current_executor.is_closed and not self.controller.cooldown_condition(current_executor, order_level) + active_and_early_stop_condition = current_executor.executor_status == PositionExecutorStatus.ACTIVE_POSITION and self.controller.early_stop_condition(current_executor, order_level) + if closed_and_not_in_cooldown: + self.store_executor(current_executor, order_level) + elif active_and_early_stop_condition: + current_executor.early_stop() + else: + position_config = self.controller.get_position_config(order_level, signal) + if position_config: + self.create_executor(position_config, order_level) diff --git a/hummingbot/smart_components/strategy_frameworks/executor_handler_base.py b/hummingbot/smart_components/strategy_frameworks/executor_handler_base.py new file mode 100644 index 0000000000..cf76d8c9f3 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/executor_handler_base.py @@ -0,0 +1,238 @@ +import asyncio +import datetime +import glob +from pathlib import Path + +import pandas as pd + +from hummingbot import data_path +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase +from hummingbot.smart_components.strategy_frameworks.data_types import ExecutorHandlerStatus, OrderLevel +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class ExecutorHandlerBase: + def __init__(self, strategy: ScriptStrategyBase, controller: ControllerBase, update_interval: float = 1.0): + """ + Initialize the ExecutorHandlerBase. + + :param strategy: The strategy instance. + :param controller: The controller instance. + :param update_interval: Update interval in seconds. + """ + self.strategy = strategy + self.controller = controller + self.update_interval = update_interval + self.terminated = asyncio.Event() + self.level_executors = {level.level_id: None for level in self.controller.config.order_levels} + self.status = ExecutorHandlerStatus.NOT_STARTED + + def start(self): + """Start the executor handler.""" + self.controller.start() + safe_ensure_future(self.control_loop()) + + def stop(self): + """Stop the executor handler.""" + self.terminated.set() + + def on_stop(self): + """Actions to perform on stop.""" + self.controller.stop() + + def on_start(self): + """Actions to perform on start.""" + pass + + async def control_task(self): + """Control task to be implemented by subclasses.""" + raise NotImplementedError + + def get_csv_path(self) -> Path: + """ + Get the CSV path for storing executor data. + + :return: Path object for the CSV. + """ + today = datetime.datetime.today() + return Path(data_path()) / f"{self.controller.get_csv_prefix()}_{today.day:02d}-{today.month:02d}-{today.year}.csv" + + def store_executor(self, executor: PositionExecutor, order_level: OrderLevel): + """ + Store executor data to CSV. + + :param executor: The executor instance. + :param order_level: The order level instance. + """ + if executor: + csv_path = self.get_csv_path() + executor_data = executor.to_json() + if not csv_path.exists(): + headers = executor_data.keys() + df_header = pd.DataFrame(columns=headers) + df_header.to_csv(csv_path, mode='a', header=True, index=False) + df = pd.DataFrame([executor_data]) + df.to_csv(csv_path, mode='a', header=False, index=False) + self.level_executors[order_level.level_id] = None + + def create_executor(self, position_config: PositionConfig, order_level: OrderLevel): + """ + Create an executor. + + :param position_config: The position configuration. + :param order_level: The order level instance. + """ + executor = PositionExecutor(self.strategy, position_config) + self.level_executors[order_level.level_id] = executor + + async def control_loop(self): + """Main control loop.""" + self.on_start() + self.status = ExecutorHandlerStatus.ACTIVE + while not self.terminated.is_set(): + await self.control_task() + await self._sleep(self.update_interval) + self.status = ExecutorHandlerStatus.TERMINATED + self.on_stop() + + def close_open_positions(self, connector_name: str = None, trading_pair: str = None): + """ + Close all open positions. + + :param connector_name: The connector name. + :param trading_pair: The trading pair. + """ + connector = self.strategy.connectors[connector_name] + for pos_key, position in connector.account_positions.items(): + if position.trading_pair == trading_pair: + action = self.strategy.sell if position.position_side == PositionSide.LONG else self.strategy.buy + action(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + + def get_closed_executors_df(self): + dfs = [pd.read_csv(file) for file in glob.glob(data_path() + f"/{self.controller.get_csv_prefix()}*")] + if len(dfs) > 0: + df = pd.concat(dfs) + return self.controller.filter_executors_df(df) + return pd.DataFrame() + + def get_active_executors_df(self) -> pd.DataFrame: + """ + Get active executors as a DataFrame. + + :return: DataFrame containing active executors. + """ + executors = [executor.to_json() for executor in self.level_executors.values() if executor] + return pd.DataFrame(executors) if executors else pd.DataFrame() + + @staticmethod + def get_executors_df(csv_prefix: str) -> pd.DataFrame: + """ + Get executors from CSV. + + :param csv_prefix: The CSV prefix. + :return: DataFrame containing executors. + """ + dfs = [pd.read_csv(file) for file in Path(data_path()).glob(f"{csv_prefix}*")] + return pd.concat(dfs) if dfs else pd.DataFrame() + + @staticmethod + def summarize_executors_df(executors_df): + if len(executors_df) > 0: + net_pnl = executors_df["net_pnl"].sum() + net_pnl_quote = executors_df["net_pnl_quote"].sum() + total_executors = executors_df.shape[0] + executors_with_position = executors_df[executors_df["net_pnl"] != 0] + total_executors_with_position = executors_with_position.shape[0] + total_volume = executors_with_position["amount"].sum() * 2 + total_long = (executors_with_position["side"] == "BUY").sum() + total_short = (executors_with_position["side"] == "SELL").sum() + correct_long = ((executors_with_position["side"] == "BUY") & (executors_with_position["net_pnl"] > 0)).sum() + correct_short = ((executors_with_position["side"] == "SELL") & (executors_with_position["net_pnl"] > 0)).sum() + accuracy_long = correct_long / total_long if total_long > 0 else 0 + accuracy_short = correct_short / total_short if total_short > 0 else 0 + + close_types = executors_df.groupby("close_type")["timestamp"].count() + return { + "net_pnl": net_pnl, + "net_pnl_quote": net_pnl_quote, + "total_executors": total_executors, + "total_executors_with_position": total_executors_with_position, + "total_volume": total_volume, + "total_long": total_long, + "total_short": total_short, + "close_types": close_types, + "accuracy_long": accuracy_long, + "accuracy_short": accuracy_short, + } + return { + "net_pnl": 0, + "net_pnl_quote": 0, + "total_executors": 0, + "total_executors_with_position": 0, + "total_volume": 0, + "total_long": 0, + "total_short": 0, + "close_types": 0, + "accuracy_long": 0, + "accuracy_short": 0, + } + + def closed_executors_info(self): + closed_executors = self.get_closed_executors_df() + return self.summarize_executors_df(closed_executors) + + def active_executors_info(self): + active_executors = self.get_active_executors_df() + return self.summarize_executors_df(active_executors) + + def to_format_status(self) -> str: + """ + Base status for executor handler. + """ + lines = [] + lines.extend(["\n################################ Active Executors ################################"]) + + for level_id, executor in self.level_executors.items(): + lines.extend([f"|Level: {level_id}"]) + if executor: + lines.extend(executor.to_format_status()) + else: + lines.extend(["| No active executor."]) + lines.extend(["\n################################## Performance ##################################"]) + closed_executors_info = self.closed_executors_info() + active_executors_info = self.active_executors_info() + unrealized_pnl = float(active_executors_info["net_pnl"]) + realized_pnl = closed_executors_info["net_pnl"] + total_pnl = unrealized_pnl + realized_pnl + total_volume = closed_executors_info["total_volume"] + float(active_executors_info["total_volume"]) + total_long = closed_executors_info["total_long"] + float(active_executors_info["total_long"]) + total_short = closed_executors_info["total_short"] + float(active_executors_info["total_short"]) + accuracy_long = closed_executors_info["accuracy_long"] + accuracy_short = closed_executors_info["accuracy_short"] + total_accuracy = (accuracy_long * total_long + accuracy_short * total_short) \ + / (total_long + total_short) if (total_long + total_short) > 0 else 0 + lines.extend([f""" +| Unrealized PNL: {unrealized_pnl * 100:.2f} % | Realized PNL: {realized_pnl * 100:.2f} % | Total PNL: {total_pnl * 100:.2f} % | Total Volume: {total_volume} +| Total positions: {total_short + total_long} --> Accuracy: {total_accuracy:.2%} + | Long: {total_long} --> Accuracy: {accuracy_long:.2%} | Short: {total_short} --> Accuracy: {accuracy_short:.2%} + +Closed executors: {closed_executors_info["total_executors"]} + {closed_executors_info["close_types"]} + """]) + lines.extend(self.controller.to_format_status()) + return "\n".join(lines) + + async def _sleep(self, delay: float): + """ + Method created to enable tests to prevent processes from sleeping + """ + await asyncio.sleep(delay) diff --git a/hummingbot/smart_components/strategy_frameworks/market_making/__init__.py b/hummingbot/smart_components/strategy_frameworks/market_making/__init__.py new file mode 100644 index 0000000000..b43dd9a77e --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/market_making/__init__.py @@ -0,0 +1,8 @@ +from .market_making_controller_base import MarketMakingControllerBase, MarketMakingControllerConfigBase +from .market_making_executor_handler import MarketMakingExecutorHandler + +__all__ = [ + "MarketMakingControllerConfigBase", + "MarketMakingControllerBase", + "MarketMakingExecutorHandler" +] diff --git a/hummingbot/smart_components/strategy_frameworks/market_making/market_making_controller_base.py b/hummingbot/smart_components/strategy_frameworks/market_making/market_making_controller_base.py new file mode 100644 index 0000000000..e1dfe6da83 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/market_making/market_making_controller_base.py @@ -0,0 +1,67 @@ +from decimal import Decimal +from typing import List, Optional, Set + +from hummingbot.core.data_type.common import PositionMode +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase, ControllerConfigBase +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel + + +class MarketMakingControllerConfigBase(ControllerConfigBase): + exchange: str + trading_pair: str + leverage: int = 10 + position_mode: PositionMode = PositionMode.HEDGE + + +class MarketMakingControllerBase(ControllerBase): + + def __init__(self, + config: MarketMakingControllerConfigBase, + excluded_parameters: Optional[List[str]] = None): + super().__init__(config, excluded_parameters) + self.config = config # this is only for type hints + + def filter_executors_df(self, df): + return df[df["trading_pair"] == self.config.trading_pair] + + def get_price_and_spread_multiplier(self): + """ + Gets the price and spread multiplier from the last candlestick. + """ + candles_df = self.get_processed_data() + return Decimal(candles_df["price_multiplier"].iloc[-1]), Decimal(candles_df["spread_multiplier"].iloc[-1]) + + def update_strategy_markets_dict(self, markets_dict: dict[str, Set] = {}): + if self.config.exchange not in markets_dict: + markets_dict[self.config.exchange] = {self.config.trading_pair} + else: + markets_dict[self.config.exchange].add(self.config.trading_pair) + return markets_dict + + @property + def is_perpetual(self): + """ + Checks if the exchange is a perpetual market. + """ + return "perpetual" in self.config.exchange + + def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + raise NotImplementedError + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + raise NotImplementedError + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + raise NotImplementedError + + def get_position_config(self, order_level: OrderLevel) -> PositionConfig: + """ + Creates a PositionConfig object from an OrderLevel object. + Here you can use technical indicators to determine the parameters of the position config. + """ + raise NotImplementedError + + def get_processed_data(self): + raise NotImplementedError diff --git a/hummingbot/smart_components/strategy_frameworks/market_making/market_making_executor_handler.py b/hummingbot/smart_components/strategy_frameworks/market_making/market_making_executor_handler.py new file mode 100644 index 0000000000..b95a298459 --- /dev/null +++ b/hummingbot/smart_components/strategy_frameworks/market_making/market_making_executor_handler.py @@ -0,0 +1,47 @@ +from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorStatus +from hummingbot.smart_components.strategy_frameworks.executor_handler_base import ExecutorHandlerBase +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import ( + MarketMakingControllerBase, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingExecutorHandler(ExecutorHandlerBase): + def __init__(self, strategy: ScriptStrategyBase, controller: MarketMakingControllerBase, + update_interval: float = 1.0): + super().__init__(strategy, controller, update_interval) + self.controller = controller + + def on_stop(self): + if self.controller.is_perpetual: + self.close_open_positions(connector_name=self.controller.config.exchange, trading_pair=self.controller.config.trading_pair) + super().on_stop() + + def on_start(self): + if self.controller.is_perpetual: + self.set_leverage_and_position_mode() + + def set_leverage_and_position_mode(self): + connector = self.strategy.connectors[self.controller.config.exchange] + connector.set_position_mode(self.controller.config.position_mode) + connector.set_leverage(trading_pair=self.controller.config.trading_pair, leverage=self.controller.config.leverage) + + async def control_task(self): + if self.controller.all_candles_ready: + for order_level in self.controller.config.order_levels: + current_executor = self.level_executors[order_level.level_id] + if current_executor: + closed_and_not_in_cooldown = current_executor.is_closed and not self.controller.cooldown_condition( + current_executor, order_level) + active_and_early_stop_condition = current_executor.executor_status == PositionExecutorStatus.ACTIVE_POSITION and self.controller.early_stop_condition( + current_executor, order_level) + order_placed_and_refresh_condition = current_executor.executor_status == PositionExecutorStatus.NOT_STARTED and self.controller.refresh_order_condition( + current_executor, order_level) + if closed_and_not_in_cooldown: + self.store_executor(current_executor, order_level) + elif active_and_early_stop_condition or order_placed_and_refresh_condition: + current_executor.early_stop() + else: + position_config = self.controller.get_position_config(order_level) + if position_config: + self.create_executor(position_config, order_level) diff --git a/hummingbot/smart_components/utils.py b/hummingbot/smart_components/utils.py new file mode 100644 index 0000000000..4576e483c0 --- /dev/null +++ b/hummingbot/smart_components/utils.py @@ -0,0 +1,52 @@ +import json +from decimal import Decimal +from enum import Enum + +import yaml + + +class ConfigEncoderDecoder: + + def __init__(self, *enum_classes): + self.enum_classes = {enum_class.__name__: enum_class for enum_class in enum_classes} + + def recursive_encode(self, value): + if isinstance(value, dict): + return {key: self.recursive_encode(val) for key, val in value.items()} + elif isinstance(value, list): + return [self.recursive_encode(val) for val in value] + elif isinstance(value, Enum): + return {"__enum__": True, "class": type(value).__name__, "value": value.name} + elif isinstance(value, Decimal): + return {"__decimal__": True, "value": str(value)} + else: + return value + + def recursive_decode(self, value): + if isinstance(value, dict): + if value.get("__enum__"): + enum_class = self.enum_classes.get(value['class']) + if enum_class: + return enum_class[value["value"]] + elif value.get("__decimal__"): + return Decimal(value["value"]) + else: + return {key: self.recursive_decode(val) for key, val in value.items()} + elif isinstance(value, list): + return [self.recursive_decode(val) for val in value] + else: + return value + + def encode(self, d): + return json.dumps(self.recursive_encode(d)) + + def decode(self, s): + return self.recursive_decode(json.loads(s)) + + def yaml_dump(self, d, file_path): + with open(file_path, 'w') as file: + yaml.dump(self.recursive_encode(d), file) + + def yaml_load(self, file_path): + with open(file_path, 'r') as file: + return self.recursive_decode(yaml.safe_load(file)) diff --git a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.py b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.py index dc8b3932c0..f47fbec187 100755 --- a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.py +++ b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.py @@ -247,7 +247,7 @@ def is_gateway_market(market_info: MarketTradingPairTuple) -> bool: return market_info.market.name in AllConnectorSettings.get_gateway_amm_connector_names() def get_conversion_rates(self, market_pair: MarketTradingPairTuple): - quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair, gas_rate_source,\ + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair, gas_rate_source, \ gas_rate = self._config_map.conversion_rate_mode.get_conversion_rates(market_pair) if quote_rate is None: self.logger().warning(f"Can't find a conversion rate for {quote_pair}") @@ -255,12 +255,12 @@ def get_conversion_rates(self, market_pair: MarketTradingPairTuple): self.logger().warning(f"Can't find a conversion rate for {base_pair}") if gas_rate is None: self.logger().warning(f"Can't find a conversion rate for {gas_pair}") - return quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair,\ + return quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair, \ gas_rate_source, gas_rate def log_conversion_rates(self): for market_pair in self._market_pairs.values(): - quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair,\ + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair, \ gas_rate_source, gas_rate = self.get_conversion_rates(market_pair) if quote_pair.split("-")[0] != quote_pair.split("-")[1]: self.logger().info(f"{quote_pair} ({quote_rate_source}) conversion rate: {PerformanceMetrics.smart_round(quote_rate)}") @@ -274,7 +274,7 @@ def oracle_status_df(self): columns = ["Source", "Pair", "Rate"] data = [] for market_pair in self._market_pairs.values(): - quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair,\ + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate, gas_pair, \ gas_rate_source, gas_rate = self.get_conversion_rates(market_pair) if quote_pair.split("-")[0] != quote_pair.split("-")[1]: data.extend([ @@ -345,7 +345,7 @@ def format_status(self) -> str: limit_orders = list(tracked_maker_orders[market_pair].values()) bid, ask = self.get_top_bid_ask(market_pair) mid_price = (bid + ask) / 2 - df = LimitOrder.to_pandas(limit_orders, mid_price) + df = LimitOrder.to_pandas(limit_orders, float(mid_price)) df_lines = str(df).split("\n") lines.extend(["", " Active maker market orders:"] + [" " + line for line in df_lines]) diff --git a/hummingbot/strategy/directional_strategy_base.py b/hummingbot/strategy/directional_strategy_base.py index 7ad07963fa..8c42b9cb58 100644 --- a/hummingbot/strategy/directional_strategy_base.py +++ b/hummingbot/strategy/directional_strategy_base.py @@ -10,8 +10,8 @@ from hummingbot.connector.connector_base import ConnectorBase from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide, TradeType from hummingbot.data_feed.candles_feed.candles_base import CandlesBase -from hummingbot.smart_components.position_executor.data_types import PositionConfig, TrailingStop -from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -165,6 +165,13 @@ def get_position_config(self): side = TradeType.BUY if signal == 1 else TradeType.SELL if self.open_order_type.is_limit_type(): price = price * (1 - signal * self.open_order_slippage_buffer) + if self.trailing_stop_activation_delta and self.trailing_stop_trailing_delta: + trailing_stop = TrailingStop( + activation_price_delta=Decimal(self.trailing_stop_activation_delta), + trailing_delta=Decimal(self.trailing_stop_trailing_delta), + ) + else: + trailing_stop = None position_config = PositionConfig( timestamp=self.current_timestamp, trading_pair=self.trading_pair, @@ -179,10 +186,7 @@ def get_position_config(self): take_profit_order_type=self.take_profit_order_type, stop_loss_order_type=self.stop_loss_order_type, time_limit_order_type=self.time_limit_order_type, - trailing_stop=TrailingStop( - activation_price_delta=Decimal(self.trailing_stop_activation_delta), - trailing_delta=Decimal(self.trailing_stop_trailing_delta) - ), + trailing_stop=trailing_stop, leverage=self.leverage, ) return position_config diff --git a/hummingbot/strategy/twap/twap.py b/hummingbot/strategy/twap/twap.py index 09d1a958f5..1fa01f3571 100644 --- a/hummingbot/strategy/twap/twap.py +++ b/hummingbot/strategy/twap/twap.py @@ -1,24 +1,16 @@ -from datetime import datetime -from decimal import Decimal import logging import statistics -from typing import ( - List, - Tuple, - Optional, - Dict -) +from datetime import datetime +from decimal import Decimal +from typing import Dict, List, Optional, Tuple from hummingbot.client.performance import PerformanceMetrics from hummingbot.connector.exchange_base import ExchangeBase from hummingbot.core.clock import Clock +from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.limit_order import LimitOrder from hummingbot.core.data_type.order_book import OrderBook -from hummingbot.core.event.events import (MarketOrderFailureEvent, - OrderCancelledEvent, - OrderExpiredEvent, - ) -from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.event.events import MarketOrderFailureEvent, OrderCancelledEvent, OrderExpiredEvent from hummingbot.core.network_iterator import NetworkStatus from hummingbot.logger import HummingbotLogger from hummingbot.strategy.conditional_execution_state import ConditionalExecutionState, RunAlwaysExecutionState @@ -167,7 +159,7 @@ def format_status(self) -> str: for market_info in self._market_infos.values(): price_provider = market_info if price_provider is not None: - df = LimitOrder.to_pandas(active_orders, mid_price=price_provider.get_mid_price()) + df = LimitOrder.to_pandas(active_orders, mid_price=float(price_provider.get_mid_price())) if self._is_buy: # Descend from the price closest to the mid price df = df.sort_values(by=['Price'], ascending=False) diff --git a/pyproject.toml b/pyproject.toml index 377cd70ece..90f13439d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ exclude = ''' ''' [build-system] -requires = ["setuptools", "wheel", "numpy", "cython==0.29.15"] +requires = ["setuptools", "wheel", "numpy", "cython==3.0.0a10"] [tool.isort] line_length = 120 diff --git a/scripts/arbitrage_with_smart_component.py b/scripts/arbitrage_with_smart_component.py index 9c71767af7..7a303a9c15 100644 --- a/scripts/arbitrage_with_smart_component.py +++ b/scripts/arbitrage_with_smart_component.py @@ -1,8 +1,8 @@ from decimal import Decimal from hummingbot.core.rate_oracle.rate_oracle import RateOracle -from hummingbot.smart_components.arbitrage_executor.arbitrage_executor import ArbitrageExecutor -from hummingbot.smart_components.arbitrage_executor.data_types import ArbitrageConfig, ExchangePair +from hummingbot.smart_components.executors.arbitrage_executor.arbitrage_executor import ArbitrageExecutor +from hummingbot.smart_components.executors.arbitrage_executor.data_types import ArbitrageConfig, ExchangePair from hummingbot.strategy.script_strategy_base import ScriptStrategyBase diff --git a/scripts/backtest_mm_example.py b/scripts/backtest_mm_example.py index 7218b83ef8..4b5d580a86 100644 --- a/scripts/backtest_mm_example.py +++ b/scripts/backtest_mm_example.py @@ -5,7 +5,7 @@ import pandas as pd from hummingbot import data_path -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -37,7 +37,7 @@ class BacktestMM(ScriptStrategyBase): execution_exchange = f"{exchange}_paper_trade" if paper_trade_enabled else exchange interval = "1m" results_df = None - candle = CandlesFactory.get_candle(connector=exchange, trading_pair=trading_pair, interval=interval, max_records=days * 60 * 24) + candle = CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval=interval, max_records=days * 60 * 24)) candle.start() csv_path = data_path() + f"/backtest_{trading_pair}_{bid_spread_bps}_bid_{ask_spread_bps}_ask.csv" diff --git a/scripts/candles_example.py b/scripts/candles_example.py index 7015980c57..f909731e46 100644 --- a/scripts/candles_example.py +++ b/scripts/candles_example.py @@ -4,7 +4,7 @@ import pandas_ta as ta # noqa: F401 from hummingbot.connector.connector_base import ConnectorBase -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -24,15 +24,9 @@ class CandlesExample(ScriptStrategyBase): # Is possible to use the Candles Factory to create the candlestick that you want, and then you have to start it. # Also, you can use the class directly like BinancePerpetualsCandles(trading_pair, interval, max_records), but # this approach is better if you want to initialize multiple candles with a list or dict of configurations. - eth_1m_candles = CandlesFactory.get_candle(connector="binance", - trading_pair="ETH-USDT", - interval="1m", max_records=500) - eth_1h_candles = CandlesFactory.get_candle(connector="binance_perpetual", - trading_pair="ETH-USDT", - interval="1h", max_records=500) - eth_1w_candles = CandlesFactory.get_candle(connector="binance_perpetual", - trading_pair="ETH-USDT", - interval="1w", max_records=50) + eth_1m_candles = CandlesFactory.get_candle(CandlesConfig(connector="binance", trading_pair="ETH-USDT", interval="1m", max_records=1000)) + eth_1h_candles = CandlesFactory.get_candle(CandlesConfig(connector="binance", trading_pair="ETH-USDT", interval="1h", max_records=1000)) + eth_1w_candles = CandlesFactory.get_candle(CandlesConfig(connector="binance", trading_pair="ETH-USDT", interval="1w", max_records=200)) # The markets are the connectors that you can use to execute all the methods of the scripts strategy base # The candlesticks are just a component that provides the information of the candlesticks diff --git a/scripts/directional_strategy_bb_rsi_multi_timeframe.py b/scripts/directional_strategy_bb_rsi_multi_timeframe.py index 38d52198b4..560318d071 100644 --- a/scripts/directional_strategy_bb_rsi_multi_timeframe.py +++ b/scripts/directional_strategy_bb_rsi_multi_timeframe.py @@ -1,6 +1,6 @@ from decimal import Decimal -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -48,14 +48,10 @@ class MultiTimeframeBBRSI(DirectionalStrategyBase): time_limit: int = None trailing_stop_activation_delta = 0.004 trailing_stop_trailing_delta = 0.001 - + CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000) candles = [ - CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="1m", max_records=150), - CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="3m", max_records=150), + CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="1m", max_records=1000)), + CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000)), ] markets = {exchange: {trading_pair}} diff --git a/scripts/directional_strategy_macd_bb.py b/scripts/directional_strategy_macd_bb.py index 7f2d3588a1..59c13d4f8d 100644 --- a/scripts/directional_strategy_macd_bb.py +++ b/scripts/directional_strategy_macd_bb.py @@ -1,6 +1,6 @@ from decimal import Decimal -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -49,9 +49,7 @@ class MacdBB(DirectionalStrategyBase): trailing_stop_activation_delta = 0.003 trailing_stop_trailing_delta = 0.0007 - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="3m", max_records=150)] + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000))] markets = {exchange: {trading_pair}} def get_signal(self): diff --git a/scripts/directional_strategy_rsi.py b/scripts/directional_strategy_rsi.py index e0d28cbc77..217e7ebb2b 100644 --- a/scripts/directional_strategy_rsi.py +++ b/scripts/directional_strategy_rsi.py @@ -1,6 +1,6 @@ from decimal import Decimal -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -54,9 +54,7 @@ class RSI(DirectionalStrategyBase): trailing_stop_trailing_delta = 0.001 cooldown_after_execution = 10 - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="1m", max_records=150)] + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000))] markets = {exchange: {trading_pair}} def get_signal(self): diff --git a/scripts/directional_strategy_rsi_spot.py b/scripts/directional_strategy_rsi_spot.py index 723d5c1407..db5f3e016c 100644 --- a/scripts/directional_strategy_rsi_spot.py +++ b/scripts/directional_strategy_rsi_spot.py @@ -1,6 +1,6 @@ from decimal import Decimal -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -53,9 +53,7 @@ class RSISpot(DirectionalStrategyBase): trailing_stop_activation_delta = 0.004 trailing_stop_trailing_delta = 0.001 - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="1m", max_records=150)] + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000))] markets = {exchange: {trading_pair}} def get_signal(self): diff --git a/scripts/directional_strategy_trend_follower.py b/scripts/directional_strategy_trend_follower.py index 857750f42b..87686a007c 100644 --- a/scripts/directional_strategy_trend_follower.py +++ b/scripts/directional_strategy_trend_follower.py @@ -1,7 +1,7 @@ from decimal import Decimal from hummingbot.core.data_type.common import OrderType -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -20,9 +20,7 @@ class TrendFollowingStrategy(DirectionalStrategyBase): take_profit_order_type: OrderType = OrderType.MARKET trailing_stop_activation_delta = 0.01 trailing_stop_trailing_delta = 0.003 - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="3m", max_records=250)] + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000))] markets = {exchange: {trading_pair}} def get_signal(self): diff --git a/scripts/directional_strategy_widening_ema_bands.py b/scripts/directional_strategy_widening_ema_bands.py index 3a1946fea5..beb3e0326e 100644 --- a/scripts/directional_strategy_widening_ema_bands.py +++ b/scripts/directional_strategy_widening_ema_bands.py @@ -1,6 +1,6 @@ from decimal import Decimal -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase @@ -50,9 +50,7 @@ class WideningEMABands(DirectionalStrategyBase): trailing_stop_activation_delta = 0.008 trailing_stop_trailing_delta = 0.003 - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="3m", max_records=150)] + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000))] markets = {exchange: {trading_pair}} def get_signal(self): diff --git a/scripts/directional_trading_macd_bb_v1.py b/scripts/directional_trading_macd_bb_v1.py new file mode 100644 index 0000000000..7bfbf2e939 --- /dev/null +++ b/scripts/directional_trading_macd_bb_v1.py @@ -0,0 +1,91 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import OrderType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.macd_bb_v1 import MACDBBV1, MACDBBV1Config +from hummingbot.smart_components.strategy_frameworks.data_types import ( + ExecutorHandlerStatus, + OrderLevel, + TripleBarrierConf, +) +from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_executor_handler import ( + DirectionalTradingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingDmanComposed(ScriptStrategyBase): + trading_pairs = ["HBAR-USDT", "CYBER-USDT", "ETH-USDT", "LPT-USDT", "UNFI-USDT"] + leverage_by_trading_pair = { + "HBAR-USDT": 25, + "CYBER-USDT": 20, + "ETH-USDT": 100, + "LPT-USDT": 10, + "UNFI-USDT": 20, + } + triple_barrier_conf = TripleBarrierConf( + stop_loss=Decimal("0.01"), take_profit=Decimal("0.03"), + time_limit=60 * 60 * 6, + trailing_stop_activation_price_delta=Decimal("0.008"), + trailing_stop_trailing_delta=Decimal("0.004"), + open_order_type=OrderType.MARKET + ) + + order_levels = [ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15), + spread_factor=Decimal(0.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15), + spread_factor=Decimal(0.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + ] + controllers = {} + markets = {} + executor_handlers = {} + + for trading_pair in trading_pairs: + config = MACDBBV1Config( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=order_levels, + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=100), + ], + leverage=leverage_by_trading_pair[trading_pair], + macd_fast=21, macd_slow=42, macd_signal=9, + bb_length=100, bb_std=2.0, bb_long_threshold=0.3, bb_short_threshold=0.7, + ) + controller = MACDBBV1(config=config) + markets = controller.update_strategy_markets_dict(markets) + controllers[trading_pair] = controller + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + for trading_pair, controller in self.controllers.items(): + self.executor_handlers[trading_pair] = DirectionalTradingExecutorHandler(strategy=self, controller=controller) + + def on_stop(self): + for executor_handler in self.executor_handlers.values(): + executor_handler.stop() + + def on_tick(self): + """ + This shows you how you can start meta controllers. You can run more than one at the same time and based on the + market conditions, you can orchestrate from this script when to stop or start them. + """ + for executor_handler in self.executor_handlers.values(): + if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED: + executor_handler.start() + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + for trading_pair, executor_handler in self.executor_handlers.items(): + if executor_handler.controller.all_candles_ready: + lines.extend( + [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}", + executor_handler.to_format_status()]) + return "\n".join(lines) diff --git a/scripts/download_candles.py b/scripts/download_candles.py index e9501fd382..4e56cc2ca2 100644 --- a/scripts/download_candles.py +++ b/scripts/download_candles.py @@ -4,7 +4,7 @@ from hummingbot import data_path from hummingbot.client.hummingbot_application import HummingbotApplication from hummingbot.connector.connector_base import ConnectorBase -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -37,9 +37,8 @@ def __init__(self, connectors: Dict[str, ConnectorBase]): self.candles = {f"{combinations[0]}_{combinations[1]}": {} for combinations in combinations} # we need to initialize the candles for each trading pair for combination in combinations: - candle = CandlesFactory.get_candle(connector=self.exchange, trading_pair=combination[0], - interval=combination[1], - max_records=self.get_max_records(self.days_to_download, combination[1])) + + candle = CandlesFactory.get_candle(CandlesConfig(connector=self.exchange, trading_pair=combination[0], interval=combination[1], max_records=self.get_max_records(self.days_to_download, combination[1]))) candle.start() # we are storing the candles object and the csv path to save the candles self.candles[f"{combination[0]}_{combination[1]}"]["candles"] = candle diff --git a/scripts/format_status_example.py b/scripts/format_status_example.py index 7ff9b1fc43..42d38c3d83 100644 --- a/scripts/format_status_example.py +++ b/scripts/format_status_example.py @@ -7,9 +7,9 @@ class FormatStatusExample(ScriptStrategyBase): Run the command status --live, once the strategy starts. """ markets = { - "binance_paper_trade": {"ETH-USDT"}, - "kucoin_paper_trade": {"ETH-USDT"}, - "gate_io_paper_trade": {"ETH-USDT"}, + "binance_paper_trade": {"ETH-USDT", "BTC-USDT", "MATIC-USDT", "AVAX-USDT"}, + "kucoin_paper_trade": {"ETH-USDT", "BTC-USDT", "MATIC-USDT", "AVAX-USDT"}, + "gate_io_paper_trade": {"ETH-USDT", "BTC-USDT", "MATIC-USDT", "AVAX-USDT"}, } def format_status(self) -> str: diff --git a/scripts/macd_bb_directional_strategy.py b/scripts/macd_bb_directional_strategy.py index 965cf53a89..e8cb192112 100644 --- a/scripts/macd_bb_directional_strategy.py +++ b/scripts/macd_bb_directional_strategy.py @@ -10,9 +10,9 @@ from hummingbot import data_path from hummingbot.connector.connector_base import ConnectorBase from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory -from hummingbot.smart_components.position_executor.data_types import PositionConfig -from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory +from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -38,9 +38,7 @@ class MACDBBDirectionalStrategy(ScriptStrategyBase): # Create the candles that we want to use and the thresholds for the indicators # IMPORTANT: The connector name of the candles can be binance or binance_perpetual, and can be different from the # connector that you define to trade - candles = CandlesFactory.get_candle(connector="binance_perpetual", - trading_pair=trading_pair, - interval="3m", max_records=150) + candles = CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000)) # Configure the leverage and order amount the bot is going to use set_leverage_flag = None diff --git a/scripts/market_making_dman_composed.py b/scripts/market_making_dman_composed.py new file mode 100644 index 0000000000..385cf6d87a --- /dev/null +++ b/scripts/market_making_dman_composed.py @@ -0,0 +1,145 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config +from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config +from hummingbot.smart_components.strategy_frameworks.data_types import ( + ExecutorHandlerStatus, + OrderLevel, + TripleBarrierConf, +) +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import ( + MarketMakingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingDmanComposed(ScriptStrategyBase): + trading_pair = "HBAR-USDT" + triple_barrier_conf_top = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 1, + trailing_stop_activation_price_delta=Decimal("0.002"), + trailing_stop_trailing_delta=Decimal("0.0005") + ) + triple_barrier_conf_bottom = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 3, + trailing_stop_activation_price_delta=Decimal("0.005"), + trailing_stop_trailing_delta=Decimal("0.001") + ) + + config_v1 = DManV1Config( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=[ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15), + spread_factor=Decimal(1.0), order_refresh_time=60 * 30, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(50), + spread_factor=Decimal(5.0), order_refresh_time=60 * 30, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50), + spread_factor=Decimal(8.0), order_refresh_time=60 * 15, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15), + spread_factor=Decimal(1.0), order_refresh_time=60 * 30, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(50), + spread_factor=Decimal(5.0), order_refresh_time=60 * 30, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50), + spread_factor=Decimal(8.0), order_refresh_time=60 * 15, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + ], + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000), + ], + leverage=25, + natr_length=21 + ) + config_v2 = DManV2Config( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=[ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(30), + spread_factor=Decimal(2.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50), + spread_factor=Decimal(3.0), order_refresh_time=60 * 15, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(30), + spread_factor=Decimal(2.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50), + spread_factor=Decimal(3.0), order_refresh_time=60 * 15, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + ], + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000), + ], + leverage=25, + natr_length=21, macd_fast=12, macd_slow=26, macd_signal=9 + ) + dman_v1 = DManV1(config=config_v1) + dman_v2 = DManV2(config=config_v2) + + empty_markets = {} + markets = dman_v1.update_strategy_markets_dict(empty_markets) + markets = dman_v2.update_strategy_markets_dict(markets) + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.dman_v1_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v1) + self.dman_v2_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v2) + + def on_stop(self): + self.close_open_positions() + + def on_tick(self): + """ + This shows you how you can start meta controllers. You can run more than one at the same time and based on the + market conditions, you can orchestrate from this script when to stop or start them. + """ + if self.dman_v1_executor.status == ExecutorHandlerStatus.NOT_STARTED: + self.dman_v1_executor.start() + if self.dman_v2_executor.status == ExecutorHandlerStatus.NOT_STARTED: + self.dman_v2_executor.start() + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + lines.extend(["DMAN V1", self.dman_v1_executor.to_format_status()]) + lines.extend(["\n-----------------------------------------\n"]) + lines.extend(["DMAN V2", self.dman_v2_executor.to_format_status()]) + return "\n".join(lines) + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if trading_pair in self.markets[connector_name]: + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) diff --git a/scripts/market_making_dman_v1.py b/scripts/market_making_dman_v1.py new file mode 100644 index 0000000000..c1c0c1f577 --- /dev/null +++ b/scripts/market_making_dman_v1.py @@ -0,0 +1,98 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config +from hummingbot.smart_components.strategy_frameworks.data_types import ( + ExecutorHandlerStatus, + OrderLevel, + TripleBarrierConf, +) +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import ( + MarketMakingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingDmanV1(ScriptStrategyBase): + trading_pair = "HBAR-USDT" + triple_barrier_conf = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 24, + trailing_stop_activation_price_delta=Decimal("0.002"), + trailing_stop_trailing_delta=Decimal("0.0005") + ) + + config_v1 = DManV1Config( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=[ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(20), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(50), + spread_factor=Decimal(2.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(20), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(50), + spread_factor=Decimal(2.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + ], + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000), + ], + leverage=10, + natr_length=21 + ) + + dman_v1 = DManV1(config=config_v1) + + empty_markets = {} + markets = dman_v1.update_strategy_markets_dict(empty_markets) + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.dman_v1_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v1) + + def on_stop(self): + self.close_open_positions() + + def on_tick(self): + """ + This shows you how you can start meta controllers. You can run more than one at the same time and based on the + market conditions, you can orchestrate from this script when to stop or start them. + """ + if self.dman_v1_executor.status == ExecutorHandlerStatus.NOT_STARTED: + self.dman_v1_executor.start() + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + lines.extend(["DMAN V1", self.dman_v1_executor.to_format_status()]) + lines.extend(["\n-----------------------------------------\n"]) + return "\n".join(lines) + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if trading_pair in self.markets[connector_name]: + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) diff --git a/scripts/market_making_dman_v2.py b/scripts/market_making_dman_v2.py new file mode 100644 index 0000000000..63325026c2 --- /dev/null +++ b/scripts/market_making_dman_v2.py @@ -0,0 +1,102 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config +from hummingbot.smart_components.strategy_frameworks.data_types import ( + ExecutorHandlerStatus, + OrderLevel, + TripleBarrierConf, +) +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import ( + MarketMakingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingDmanV2(ScriptStrategyBase): + trading_pair = "HBAR-USDT" + triple_barrier_conf = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 24, + trailing_stop_activation_price_delta=Decimal("0.002"), + trailing_stop_trailing_delta=Decimal("0.0005") + ) + config_v2 = DManV2Config( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=[ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15), + spread_factor=Decimal(0.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(30), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50), + spread_factor=Decimal(2.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15), + spread_factor=Decimal(0.5), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(30), + spread_factor=Decimal(1.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50), + spread_factor=Decimal(2.0), order_refresh_time=60 * 5, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf), + ], + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000), + ], + leverage=10, + natr_length=21, macd_fast=12, macd_slow=26, macd_signal=9 + ) + dman_v2 = DManV2(config=config_v2) + + empty_markets = {} + markets = dman_v2.update_strategy_markets_dict(empty_markets) + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.dman_v2_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v2) + + def on_stop(self): + self.close_open_positions() + + def on_tick(self): + """ + This shows you how you can start meta controllers. You can run more than one at the same time and based on the + market conditions, you can orchestrate from this script when to stop or start them. + """ + if self.dman_v2_executor.status == ExecutorHandlerStatus.NOT_STARTED: + self.dman_v2_executor.start() + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + lines.extend(["DMAN V2", self.dman_v2_executor.to_format_status()]) + lines.extend(["\n-----------------------------------------\n"]) + return "\n".join(lines) + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if trading_pair in self.markets[connector_name]: + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) diff --git a/scripts/market_making_multiple_controllers.py b/scripts/market_making_multiple_controllers.py new file mode 100644 index 0000000000..486f75f52d --- /dev/null +++ b/scripts/market_making_multiple_controllers.py @@ -0,0 +1,172 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase, TradeType +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.controllers.bollinger_grid import BollingerGrid, BollingerGridConfig +from hummingbot.smart_components.strategy_frameworks.data_types import ( + ExecutorHandlerStatus, + OrderLevel, + TripleBarrierConf, +) +from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import ( + MarketMakingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketMakingMultipleControllers(ScriptStrategyBase): + trading_pairs_v2 = ["ARKM-USDT", "BAKE-USDT", "SUI-USDT"] + + leverage_by_trading_pair = { + "HBAR-USDT": 25, + "CYBER-USDT": 20, + "ETH-USDT": 100, + "LPT-USDT": 10, + "UNFI-USDT": 20, + "BAKE-USDT": 20, + "YGG-USDT": 20, + "SUI-USDT": 50, + "TOMO-USDT": 25, + "RUNE-USDT": 25, + "STX-USDT": 25, + "API3-USDT": 20, + "LIT-USDT": 20, + "PERP-USDT": 16, + "HOOK-USDT": 20, + "AMB-USDT": 20, + "ARKM-USDT": 20, + } + + triple_barrier_conf_top = TripleBarrierConf( + stop_loss=Decimal("0.07"), take_profit=Decimal("0.005"), + time_limit=60 * 60 * 12, + take_profit_order_type=OrderType.LIMIT, + ) + triple_barrier_conf_bottom = TripleBarrierConf( + stop_loss=Decimal("0.07"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 12, + trailing_stop_activation_price_delta=Decimal("0.01"), + trailing_stop_trailing_delta=Decimal("0.005") + ) + + order_amount_usd = 15 + order_levels = [ + OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(0.2), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(0.8), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(1.6), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=3, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(2.2), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=4, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(3.0), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=5, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(4.4), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=6, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(5.6), order_refresh_time=60 * 75, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=7, side=TradeType.BUY, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(7.2), order_refresh_time=60 * 75, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + + OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(0.2), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(0.8), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(1.6), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=3, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(2.2), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top), + OrderLevel(level=4, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(3.0), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=5, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(4.4), order_refresh_time=60 * 45, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=6, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(5.6), order_refresh_time=60 * 75, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + OrderLevel(level=7, side=TradeType.SELL, order_amount_usd=Decimal(order_amount_usd), + spread_factor=Decimal(7.2), order_refresh_time=60 * 75, + cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom), + ] + controllers = {} + markets = {} + executor_handlers = {} + + for trading_pair in trading_pairs_v2: + config = BollingerGridConfig( + exchange="binance_perpetual", + trading_pair=trading_pair, + order_levels=order_levels, + candles_config=[ + CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="15m", + max_records=300), + ], + leverage=leverage_by_trading_pair[trading_pair], + bb_length=100, + natr_length=100, + ) + controller = BollingerGrid(config=config) + markets = controller.update_strategy_markets_dict(markets) + controllers[trading_pair] = controller + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + for trading_pair, controller in self.controllers.items(): + self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller) + + def on_stop(self): + self.close_open_positions() + + def on_tick(self): + """ + This shows you how you can start meta controllers. You can run more than one at the same time and based on the + market conditions, you can orchestrate from this script when to stop or start them. + """ + for executor_handler in self.executor_handlers.values(): + if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED: + executor_handler.start() + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + for trading_pair, executor_handler in self.executor_handlers.items(): + lines.extend( + [f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}", + executor_handler.to_format_status()]) + return "\n".join(lines) + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if trading_pair in self.markets[connector_name]: + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) diff --git a/scripts/pmm_with_position_executor.py b/scripts/pmm_with_position_executor.py index f2305d3d84..0052e9c4bb 100644 --- a/scripts/pmm_with_position_executor.py +++ b/scripts/pmm_with_position_executor.py @@ -9,14 +9,14 @@ from hummingbot import data_path from hummingbot.connector.connector_base import ConnectorBase from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide, PriceType, TradeType -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory -from hummingbot.smart_components.position_executor.data_types import ( +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory +from hummingbot.smart_components.executors.position_executor.data_types import ( CloseType, PositionConfig, PositionExecutorStatus, TrailingStop, ) -from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -56,9 +56,7 @@ class PMMWithPositionExecutor(ScriptStrategyBase): trailing_stop_trailing_delta = 0.001 # Here you can use for example the LastTrade price to use in your strategy price_source = PriceType.MidPrice - candles = [CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="15m") + candles = [CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000)) ] # Configure the leverage and order amount the bot is going to use diff --git a/scripts/pmm_with_shifted_mid_dynamic_spreads.py b/scripts/pmm_with_shifted_mid_dynamic_spreads.py index 020fb4ce6c..3bbd4389bd 100644 --- a/scripts/pmm_with_shifted_mid_dynamic_spreads.py +++ b/scripts/pmm_with_shifted_mid_dynamic_spreads.py @@ -8,7 +8,7 @@ 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, OrderFilledEvent, SellOrderCompletedEvent -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -43,9 +43,7 @@ class PMMhShiftedMidPriceDynamicSpread(ScriptStrategyBase): exchange = "binance" # Creating instance of the candles - candles = CandlesFactory.get_candle(connector=exchange, - trading_pair=trading_pair, - interval="3m") + candles = CandlesFactory.get_candle(CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="3m", max_records=1000)) # Variables to store the volume and quantity of orders diff --git a/setup.py b/setup.py index 54c37abd41..bd1db9c058 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import os import subprocess import sys +import fnmatch import numpy as np from setuptools import find_packages, setup @@ -16,7 +17,7 @@ else: os.environ["CFLAGS"] = "-std=c++11" -if os.environ.get('WITHOUT_CYTHON_OPTIMIZATIONS'): +if os.environ.get("WITHOUT_CYTHON_OPTIMIZATIONS"): os.environ["CFLAGS"] += " -O0" @@ -25,15 +26,21 @@ # for C/ObjC but not for C++ class BuildExt(build_ext): def build_extensions(self): - if os.name != "nt" and '-Wstrict-prototypes' in self.compiler.compiler_so: - self.compiler.compiler_so.remove('-Wstrict-prototypes') + if os.name != "nt" and "-Wstrict-prototypes" in self.compiler.compiler_so: + self.compiler.compiler_so.remove("-Wstrict-prototypes") super().build_extensions() def main(): cpu_count = os.cpu_count() or 8 - version = "20230828" - packages = find_packages(include=["hummingbot", "hummingbot.*"]) + version = "20230930" + all_packages = find_packages(include=["hummingbot", "hummingbot.*"], ) + excluded_paths = ["hummingbot.connector.exchange.injective_v2", + "hummingbot.connector.derivative.injective_v2_perpetual", + "hummingbot.connector.gateway.clob_spot.data_sources.injective", + "hummingbot.connector.gateway.clob_perp.data_sources.injective_perpetual" + ] + packages = [pkg for pkg in all_packages if not any(fnmatch.fnmatch(pkg, pattern) for pattern in excluded_paths)] package_data = { "hummingbot": [ "core/cpp/*", @@ -42,27 +49,25 @@ def main(): ], } install_requires = [ - "0x-contract-addresses", - "0x-contract-wrappers", - "0x-order-utils", + "bidict", "aioconsole", "aiohttp", + "aioprocessing", "asyncssh", "appdirs", "appnope", "async-timeout", - "bidict", "base58", + "gql", "cachetools", "certifi", "coincurve", "cryptography", - "cython", + "cython==3.0.0", "cytoolz", "commlib-py", "docker", "diff-cover", - "dydx-python", "dydx-v3-python", "eip712-structs", "eth-abi", @@ -74,11 +79,9 @@ def main(): "ethsnarks-loopring", "flake8", "gql", - "grpcio", - "grpcio-tools" "hexbytes", "importlib-metadata", - "injective-py" + "injective-py", "mypy-extensions", "nose", "nose-exclude", @@ -87,6 +90,10 @@ def main(): "pip", "pre-commit", "prompt-toolkit", + "protobuf", + "gql", + "grpcio", + "grpcio-tools", "psutil", "pydantic", "pyjwt", @@ -108,6 +115,8 @@ def main(): "web3", "websockets", "yarl", + "python-telegram-bot==12.8", + "pandas_ta==0.3.14b", ] cython_kwargs = { @@ -117,13 +126,14 @@ def main(): cython_sources = ["hummingbot/**/*.pyx"] - if os.environ.get('WITHOUT_CYTHON_OPTIMIZATIONS'): - compiler_directives = { + compiler_directives = { + "annotation_typing": False, + } + if os.environ.get("WITHOUT_CYTHON_OPTIMIZATIONS"): + compiler_directives.update({ "optimize.use_switch": False, "optimize.unpack_method_calls": False, - } - else: - compiler_directives = {} + }) if is_posix: cython_kwargs["nthreads"] = cpu_count @@ -153,10 +163,9 @@ def main(): np.get_include() ], scripts=[ - "bin/hummingbot.py", "bin/hummingbot_quickstart.py" ], - cmdclass={'build_ext': BuildExt}, + cmdclass={"build_ext": BuildExt}, ) diff --git a/setup/environment.yml b/setup/environment.yml index b6b775b8a4..5404e71bfa 100644 --- a/setup/environment.yml +++ b/setup/environment.yml @@ -5,6 +5,7 @@ channels: dependencies: - bidict - coverage + - cython=3.0 - grpcio-tools - nomkl - nose=1.3.7 @@ -36,15 +37,16 @@ dependencies: - cachetools==4.0.0 - commlib-py==0.10.6 - cryptography==3.4.7 - - cython==3.0.0a10 - diff-cover - docker==5.0.3 - eip712-structs==1.1.0 + - dotmap==1.3.30 - ethsnarks-loopring==0.1.5 - flake8==3.7.9 - gql - importlib-metadata==0.23 - - injective-py==0.7.* + - injective-py==0.8.* + - jsonpickle==3.0.1 - mypy-extensions==0.4.3 - pandas_ta==0.3.14b - pre-commit==2.18.1 diff --git a/start b/start index d048c725e6..1fba97b78d 100755 --- a/start +++ b/start @@ -1,8 +1,31 @@ #!/bin/bash -# Check if bin/hummingbot.py exists -if [[ ! -f bin/hummingbot.py ]]; then - echo "Error: bin/hummingbot.py command not found. Make sure you are in the Hummingbot root directory" +PASSWORD="" +FILENAME="" + +# Argument parsing +while getopts ":p:f:" opt; do + case $opt in + p) + PASSWORD="$OPTARG" + ;; + f) + FILENAME="$OPTARG" + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +# Check if bin/hummingbot_quickstart.py exists +if [[ ! -f bin/hummingbot_quickstart.py ]]; then + echo "Error: bin/hummingbot_quickstart.py command not found. Make sure you are in the Hummingbot root directory" exit 1 fi @@ -12,5 +35,26 @@ if [[ $CONDA_DEFAULT_ENV != "hummingbot" ]]; then exit 1 fi -# Run bin/hummingbot.py and append errors to logs/errors.log -./bin/hummingbot.py 2>> ./logs/errors.log \ No newline at end of file +# Build the command to run +CMD="./bin/hummingbot_quickstart.py" +if [[ ! -z "$PASSWORD" ]]; then + CMD="$CMD -p \"$PASSWORD\"" +fi +if [[ ! -z "$FILENAME" ]]; then + CMD="$CMD -f \"$FILENAME\"" +fi + +# Clear the errors.log file first before executing +> ./logs/errors.log + +# Execute the command +eval $CMD 2>> ./logs/errors.log + +# Check errors.log for specific errors +if grep -q "Invalid password" ./logs/errors.log; then + echo "Error: Incorrect password provided." + exit 1 +elif grep -q "FileNotFoundError" ./logs/errors.log; then + echo "Error: Invalid file or filename provided." + exit 2 +fi diff --git a/test/hummingbot/client/test_settings.py b/test/hummingbot/client/test_settings.py index 535fdef3de..85c03439b5 100644 --- a/test/hummingbot/client/test_settings.py +++ b/test/hummingbot/client/test_settings.py @@ -8,6 +8,7 @@ from hummingbot.connector.gateway.clob_spot.data_sources.injective.injective_api_data_source import ( InjectiveAPIDataSource, ) +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_api_data_source import KujiraAPIDataSource from hummingbot.core.data_type.trade_fee import TradeFeeSchema @@ -150,3 +151,47 @@ def test_conn_init_parameters_for_gateway_injective_connector( self.assertIsInstance(api_data_source, InjectiveAPIDataSource) self.assertEqual(expected_params_without_api_data_source, params) + + @patch("hummingbot.client.settings.GatewayConnectionSetting.get_connector_spec_from_market_name") + def test_conn_init_parameters_for_gateway_kujira_connector( + self, get_connector_spec_from_market_name_mock: MagicMock + ): + get_connector_spec_from_market_name_mock.return_value = { + "connector": "kujira", + "chain": "kujira", + "network": "mainnet", + "trading_type": "CLOB_SPOT", + "wallet_address": "0xA86b66F4e7DC45a943D71a11c7DDddE341246682", # noqa: mock + "additional_spenders": [], + } + conn_settings = ConnectorSetting( + name="kujira_kujira_mainnet", + type=ConnectorType.CLOB_SPOT, + example_pair="KUJI-DEMO", + centralised=True, + use_ethereum_wallet=False, + trade_fee_schema=TradeFeeSchema(), + config_keys=None, + is_sub_domain=False, + parent_name=None, + domain_parameter=None, + use_eth_gas_lookup=False, + ) + + expected_params_without_api_data_source = { + "connector_name": "kujira", + "chain": "kujira", + "network": "mainnet", + "address": "0xA86b66F4e7DC45a943D71a11c7DDddE341246682", # noqa: mock + "trading_pairs": [], + "trading_required": False, + "client_config_map": None, + } + params = conn_settings.conn_init_parameters() + + self.assertIn("api_data_source", params) + + api_data_source = params.pop("api_data_source") + + self.assertIsInstance(api_data_source, KujiraAPIDataSource) + self.assertEqual(expected_params_without_api_data_source, params) diff --git a/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_delegated_account.py b/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_delegated_account.py index 5c320aecf3..60a403c626 100644 --- a/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_delegated_account.py +++ b/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_delegated_account.py @@ -13,6 +13,7 @@ from bidict import bidict from grpc import RpcError from pyinjective import Address, PrivateKey +from pyinjective.composer import Composer from pyinjective.orderhash import OrderHashResponse from hummingbot.client.config.client_config_map import ClientConfigMap @@ -515,6 +516,9 @@ def create_exchange_instance(self): exchange._data_source._query_executor = ProgrammableQueryExecutor() exchange._data_source._spot_market_and_trading_pair_map = bidict() exchange._data_source._derivative_market_and_trading_pair_map = bidict({self.market_id: self.trading_pair}) + + exchange._data_source._composer = Composer(network=exchange._data_source.network_name) + return exchange def validate_auth_credentials_present(self, request_call: RequestCall): diff --git a/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_offchain_vault.py b/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_offchain_vault.py index df48e1b0ea..1d3d04e20b 100644 --- a/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_offchain_vault.py +++ b/test/hummingbot/connector/derivative/injective_v2_perpetual/test_injective_v2_perpetual_derivative_for_offchain_vault.py @@ -14,6 +14,7 @@ from bidict import bidict from grpc import RpcError from pyinjective import Address, PrivateKey +from pyinjective.composer import Composer from hummingbot.client.config.client_config_map import ClientConfigMap from hummingbot.client.config.config_helpers import ClientConfigAdapter @@ -504,6 +505,9 @@ def create_exchange_instance(self): exchange._data_source._query_executor = ProgrammableQueryExecutor() exchange._data_source._spot_market_and_trading_pair_map = bidict() exchange._data_source._derivative_market_and_trading_pair_map = bidict({self.market_id: self.trading_pair}) + + exchange._data_source._composer = Composer(network=exchange._data_source.network_name) + return exchange def validate_auth_credentials_present(self, request_call: RequestCall): diff --git a/test/hummingbot/connector/exchange/injective_v2/data_sources/test_injective_data_source.py b/test/hummingbot/connector/exchange/injective_v2/data_sources/test_injective_data_source.py index b2f39b1f28..0986539c3b 100644 --- a/test/hummingbot/connector/exchange/injective_v2/data_sources/test_injective_data_source.py +++ b/test/hummingbot/connector/exchange/injective_v2/data_sources/test_injective_data_source.py @@ -7,7 +7,8 @@ from unittest import TestCase from unittest.mock import patch -from pyinjective.constant import Network +from pyinjective.composer import Composer +from pyinjective.core.network import Network from pyinjective.wallet import Address, PrivateKey from hummingbot.connector.exchange.injective_v2 import injective_constants as CONSTANTS @@ -391,6 +392,8 @@ def setUp(self, _) -> None: self.query_executor = ProgrammableQueryExecutor() self.data_source._query_executor = self.query_executor + self.data_source._composer = Composer(network=self.data_source.network_name) + def tearDown(self) -> None: self.async_run_with_timeout(self.data_source.stop()) for task in self.async_tasks: @@ -495,7 +498,8 @@ def test_order_cancel_message_generation(self): market = self._inj_usdt_market_info() orders_data = [] - order_data = self.data_source.composer.OrderData( + composer = asyncio.get_event_loop().run_until_complete(self.data_source.composer()) + order_data = composer.OrderData( market_id=market["marketId"], subaccount_id="1", order_hash="0xba954bc613a81cd712b9ec0a3afbfc94206cf2ff8c60d1868e031d59ea82bf27", # noqa: mock" @@ -504,9 +508,11 @@ def test_order_cancel_message_generation(self): ) orders_data.append(order_data) - message = self.data_source._order_cancel_message( - spot_orders_to_cancel=orders_data, - derivative_orders_to_cancel=[], + message = self.async_run_with_timeout( + self.data_source._order_cancel_message( + spot_orders_to_cancel=orders_data, + derivative_orders_to_cancel=[], + ) ) pub_key = self._grantee_private_key.to_public_key() diff --git a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_delegated_account.py b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_delegated_account.py index c4d528bec9..cdf585226f 100644 --- a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_delegated_account.py +++ b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_delegated_account.py @@ -12,6 +12,7 @@ from aioresponses.core import RequestCall from bidict import bidict from grpc import RpcError +from pyinjective.composer import Composer from pyinjective.orderhash import OrderHashManager, OrderHashResponse from pyinjective.wallet import Address, PrivateKey @@ -406,6 +407,9 @@ def create_exchange_instance(self): exchange._data_source._query_executor = ProgrammableQueryExecutor() exchange._data_source._spot_market_and_trading_pair_map = bidict({self.market_id: self.trading_pair}) exchange._data_source._derivative_market_and_trading_pair_map = bidict() + + exchange._data_source._composer = Composer(network=exchange._data_source.network_name) + return exchange def validate_auth_credentials_present(self, request_call: RequestCall): diff --git a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_offchain_vault.py b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_offchain_vault.py index 2304caa53a..1fc54222cf 100644 --- a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_offchain_vault.py +++ b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_exchange_for_offchain_vault.py @@ -12,6 +12,7 @@ from aioresponses.core import RequestCall from bidict import bidict from grpc import RpcError +from pyinjective.composer import Composer from pyinjective.wallet import Address, PrivateKey from hummingbot.client.config.client_config_map import ClientConfigMap @@ -397,6 +398,9 @@ def create_exchange_instance(self): exchange._data_source._query_executor = ProgrammableQueryExecutor() exchange._data_source._spot_market_and_trading_pair_map = bidict({self.market_id: self.trading_pair}) exchange._data_source._derivative_market_and_trading_pair_map = bidict() + + exchange._data_source._composer = Composer(network=exchange._data_source.network_name) + return exchange def validate_auth_credentials_present(self, request_call: RequestCall): diff --git a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_utils.py b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_utils.py index 6b8063b6e1..e4441a705b 100644 --- a/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_utils.py +++ b/test/hummingbot/connector/exchange/injective_v2/test_injective_v2_utils.py @@ -1,9 +1,8 @@ from unittest import TestCase from pyinjective import Address, PrivateKey -from pyinjective.constant import Network +from pyinjective.core.network import Network -import hummingbot.connector.exchange.injective_v2.injective_v2_utils as utils from hummingbot.connector.exchange.injective_v2 import injective_constants as CONSTANTS from hummingbot.connector.exchange.injective_v2.data_sources.injective_grantee_data_source import ( InjectiveGranteeDataSource, @@ -25,7 +24,6 @@ class InjectiveConfigMapTests(TestCase): def test_mainnet_network_config_creation(self): network_config = InjectiveMainnetNetworkMode() - network_config.node = "lb" network = network_config.network() expected_network = Network.mainnet(node="lb") @@ -34,31 +32,6 @@ def test_mainnet_network_config_creation(self): self.assertEqual(expected_network.lcd_endpoint, network.lcd_endpoint) self.assertTrue(network_config.use_secure_connection()) - network_config = InjectiveMainnetNetworkMode() - network_config.node = "sentry0" - - network = network_config.network() - expected_network = Network.mainnet(node="sentry0") - - self.assertEqual(expected_network.string(), network.string()) - self.assertEqual(expected_network.lcd_endpoint, network.lcd_endpoint) - self.assertFalse(network_config.use_secure_connection()) - - def test_mainnet_network_config_creation_fails_with_wrong_node(self): - network_config = InjectiveMainnetNetworkMode() - network_config.node = "lb" - network_config.node = "sentry0" - network_config.node = "sentry1" - network_config.node = "sentry3" - - with self.assertRaises(ValueError) as exception_context: - network_config.node = "invalid" - - self.assertIn( - f"invalid is not a valid node ({utils.MAINNET_NODES})", - str(exception_context.exception) - ) - def test_testnet_network_config_creation(self): network_config = InjectiveTestnetNetworkMode(testnet_node="sentry") @@ -142,7 +115,6 @@ def test_injective_vault_account_config_creation(self): def test_injective_config_creation(self): network_config = InjectiveMainnetNetworkMode() - network_config.node = "lb" _, grantee_private_key = PrivateKey.generate() _, granter_private_key = PrivateKey.generate() diff --git a/test/hummingbot/connector/exchange/polkadex/programmable_query_executor.py b/test/hummingbot/connector/exchange/polkadex/programmable_query_executor.py index a3d5beaea0..e0ea09828e 100644 --- a/test/hummingbot/connector/exchange/polkadex/programmable_query_executor.py +++ b/test/hummingbot/connector/exchange/polkadex/programmable_query_executor.py @@ -16,6 +16,8 @@ def __init__(self): self._cancel_order_responses = asyncio.Queue() self._order_history_responses = asyncio.Queue() self._order_responses = asyncio.Queue() + self._list_orders_responses = asyncio.Queue() + self._order_fills_responses = asyncio.Queue() self._order_book_update_events = asyncio.Queue() self._public_trades_update_events = asyncio.Queue() @@ -52,6 +54,7 @@ async def cancel_order( self, order_id: str, market_symbol: str, + main_address: str, proxy_address: str, signature: Dict[str, Any], ) -> Dict[str, Any]: @@ -68,6 +71,16 @@ async def find_order_by_main_account(self, main_account: str, market_symbol: str response = await self._order_responses.get() return response + async def list_open_orders_by_main_account(self, main_account: str) -> Dict[str, Any]: + response = await self._list_orders_responses.get() + return response + + async def get_order_fills_by_main_account( + self, from_timestamp: float, to_timestamp: float, main_account: str + ) -> Dict[str, Any]: + response = await self._order_fills_responses.get() + return response + async def listen_to_orderbook_updates(self, events_handler: Callable, market_symbol: str): while True: event = await self._order_book_update_events.get() diff --git a/test/hummingbot/connector/exchange/polkadex/test_polkadex_api_order_book_data_source.py b/test/hummingbot/connector/exchange/polkadex/test_polkadex_api_order_book_data_source.py index b47fd04456..099fc0fbb5 100644 --- a/test/hummingbot/connector/exchange/polkadex/test_polkadex_api_order_book_data_source.py +++ b/test/hummingbot/connector/exchange/polkadex/test_polkadex_api_order_book_data_source.py @@ -111,15 +111,15 @@ def is_logged(self, log_level: str, message: Union[str, re.Pattern]) -> bool: def test_get_new_order_book_successful(self): data = [ - {"side": "Ask", "p": 9487.5, "q": 522147, "s": "Ask"}, - {"side": "Bid", "p": 9487, "q": 336241, "s": "Bid"}, + {"side": "Ask", "p": 9487.5, "q": 522147, "s": "Ask", "stid": 1}, + {"side": "Bid", "p": 9487, "q": 336241, "s": "Bid", "stid": 1}, ] order_book_snapshot = {"getOrderbook": {"items": data}} self.data_source._data_source._query_executor._order_book_snapshots.put_nowait(order_book_snapshot) order_book = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair)) - expected_update_id = -1 + expected_update_id = 1 self.assertEqual(expected_update_id, order_book.snapshot_uid) bids = list(order_book.bid_entries()) @@ -144,10 +144,8 @@ def test_listen_for_trades_cancelled_when_listening(self): self.async_run_with_timeout(self.data_source.listen_for_trades(self.async_loop, msg_queue)) def test_listen_for_trades_logs_exception(self): - incorrect_message = {} - mock_queue = AsyncMock() - mock_queue.get.side_effect = [incorrect_message, asyncio.CancelledError()] + mock_queue.get.side_effect = [Exception("some error"), asyncio.CancelledError()] self.data_source._message_queue[self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -164,15 +162,16 @@ def test_listen_for_trades_logs_exception(self): ) def test_listen_for_trades_successful(self): + expected_trade_id = "1664193952989" trade_data = { "type": "TradeFormat", "m": self.ex_trading_pair, + "m_side": "Ask", + "trade_id": expected_trade_id, "p": "1718.5", - "vq": "17185", "q": "10", - "tid": "111", "t": 1664193952989, - "sid": "16", + "stid": "16", } trade_event = {"websocket_streams": {"data": json.dumps(trade_data)}} @@ -186,7 +185,7 @@ def test_listen_for_trades_successful(self): msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(OrderBookMessageType.TRADE, msg.type) - self.assertEqual(trade_data["tid"], msg.trade_id) + self.assertEqual(expected_trade_id, msg.trade_id) self.assertEqual(trade_data["t"] * 1e-3, msg.timestamp) expected_price = Decimal(trade_data["p"]) expected_amount = Decimal(trade_data["q"]) @@ -206,10 +205,8 @@ def test_listen_for_order_book_diffs_cancelled(self): self.async_run_with_timeout(self.data_source.listen_for_order_book_diffs(self.async_loop, msg_queue)) def test_listen_for_order_book_diffs_logs_exception(self): - incorrect_message = {} - mock_queue = AsyncMock() - mock_queue.get.side_effect = [incorrect_message, asyncio.CancelledError()] + mock_queue.get.side_effect = [Exception("some error"), asyncio.CancelledError()] self.data_source._message_queue[self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -230,12 +227,14 @@ def test_listen_for_order_book_diffs_successful(self, time_mock): time_mock.return_value = 1640001112.223 order_book_data = { - "type": "IncOb", - "changes": [ - ["Bid", "2999", "8", 4299950], - ["Bid", "1.671", "52.952", 4299951], - ["Ask", "3001", "0", 4299952], - ], + "i": 1, + "a": { + "3001": "0", + }, + "b": { + "2999": "8", + "1.671": "52.952", + }, } order_book_event = {"websocket_streams": {"data": json.dumps(order_book_data)}} @@ -252,7 +251,7 @@ def test_listen_for_order_book_diffs_successful(self, time_mock): self.assertEqual(OrderBookMessageType.DIFF, msg.type) self.assertEqual(-1, msg.trade_id) self.assertEqual(time_mock.return_value, msg.timestamp) - expected_update_id = order_book_data["changes"][-1][-1] + expected_update_id = 1 self.assertEqual(expected_update_id, msg.update_id) bids = msg.bids diff --git a/test/hummingbot/connector/exchange/polkadex/test_polkadex_exchange.py b/test/hummingbot/connector/exchange/polkadex/test_polkadex_exchange.py index 2f741e6920..ea07f2ec11 100644 --- a/test/hummingbot/connector/exchange/polkadex/test_polkadex_exchange.py +++ b/test/hummingbot/connector/exchange/polkadex/test_polkadex_exchange.py @@ -9,6 +9,7 @@ from aioresponses import aioresponses from aioresponses.core import RequestCall from bidict import bidict +from gql.transport.exceptions import TransportQueryError from substrateinterface import SubstrateInterface from hummingbot.client.config.client_config_map import ClientConfigMap @@ -31,6 +32,9 @@ class PolkadexExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): + client_order_id_prefix = "0x" + exchange_order_id_prefix = "0x" + @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -47,6 +51,8 @@ def setUp(self) -> None: self.exchange._data_source.logger().setLevel(1) self.exchange._data_source.logger().addHandler(self) self.exchange._set_trading_pair_symbol_map(bidict({self.exchange_trading_pair: self.trading_pair})) + exchange_base, exchange_quote = self.trading_pair.split("-") + self.exchange._data_source._assets_map = {exchange_base: self.base_asset, "1": self.quote_asset} def tearDown(self) -> None: super().tearDown() @@ -196,7 +202,7 @@ def trading_rules_request_erroneous_mock_response(self): @property def order_creation_request_successful_mock_response(self): - return {"place_order": self.expected_exchange_order_id} + return {"place_order": json.dumps({"is_success": True, "body": self.expected_exchange_order_id})} @property def balance_request_mock_response_for_base_and_quote(self): @@ -291,7 +297,7 @@ def expected_exchange_order_id(self): @property def is_order_fill_http_update_included_in_status_update(self) -> bool: - return False + return True @property def is_order_fill_http_update_executed_during_websocket_order_event_processing(self) -> bool: @@ -308,13 +314,25 @@ def expected_partial_fill_amount(self) -> Decimal: @property def expected_partial_fill_fee(self) -> TradeFeeBase: return AddedToCostTradeFee( - percent_token=self.quote_asset, flat_fees=[TokenAmount(token=self.quote_asset, amount=Decimal("10"))] + percent_token=self.quote_asset, + flat_fees=[ + TokenAmount( + token=self.quote_asset, + amount=Decimal("0"), # according to Polkadex team, fees will be zero for the foreseeable future + ), + ], ) @property def expected_fill_fee(self) -> TradeFeeBase: return AddedToCostTradeFee( - percent_token=self.quote_asset, flat_fees=[TokenAmount(token=self.quote_asset, amount=Decimal("30"))] + percent_token=self.quote_asset, + flat_fees=[ + TokenAmount( + token=self.quote_asset, + amount=Decimal("0"), # according to Polkadex team, fees will be zero for the foreseeable future + ), + ], ) @property @@ -394,7 +412,28 @@ def configure_order_not_found_error_cancelation_response( "0x1b99cba5555ad0ba890756fe16e499cb884b46a165b89bdce77ee8913b55ffff" # noqa: mock '\\"}","errorType":"Lambda:Handled"}', } - not_found_exception = IOError(str(not_found_error)) + not_found_exception = TransportQueryError(str(not_found_error)) + mock_queue = AsyncMock() + mock_queue.get.side_effect = partial( + self._callback_wrapper_with_response, callback=callback, response=not_found_exception + ) + self.exchange._data_source._query_executor._cancel_order_responses = mock_queue + return "" + + def configure_order_not_active_error_cancelation_response( + self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None + ) -> str: + not_found_error = { + "path": ["cancel_order"], + "data": None, + "errorType": "Lambda:Unhandled", + "errorInfo": None, + "locations": [{"line": 2, "column": 3, "sourceName": None}], + "message": '{"errorMessage":"{\\"code\\":-32000,\\"message\\":\\"Order is not active: ' + "0x1b99cba5555ad0ba890756fe16e499cb884b46a165b89bdce77ee8913b55ffff" # noqa: mock + '\\"}","errorType":"Lambda:Handled"}', + } + not_found_exception = TransportQueryError(str(not_found_error)) mock_queue = AsyncMock() mock_queue.get.side_effect = partial( self._callback_wrapper_with_response, callback=callback, response=not_found_exception @@ -424,6 +463,7 @@ def configure_completely_filled_order_status_response( def configure_canceled_order_status_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> List[str]: + self.configure_no_fills_trade_response() order_history_response = {"listOrderHistorybyMainAccount": {"items": []}} self.exchange._data_source._query_executor._order_history_responses.put_nowait(order_history_response) response = self._order_status_request_canceled_mock_response(order=order) @@ -435,6 +475,7 @@ def configure_canceled_order_status_response( def configure_open_order_status_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> List[str]: + self.configure_no_fills_trade_response() order_history_response = {"listOrderHistorybyMainAccount": {"items": []}} self.exchange._data_source._query_executor._order_history_responses.put_nowait(order_history_response) response = self._order_status_request_open_mock_response(order=order) @@ -446,6 +487,7 @@ def configure_open_order_status_response( def configure_http_error_order_status_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: + self.configure_no_fills_trade_response() order_history_response = {"listOrderHistorybyMainAccount": {"items": []}} self.exchange._data_source._query_executor._order_history_responses.put_nowait(order_history_response) mock_queue = AsyncMock() @@ -467,6 +509,7 @@ def configure_partially_filled_order_status_response( def configure_order_not_found_error_order_status_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> List[str]: + self.configure_no_fills_trade_response() order_history_response = {"listOrderHistorybyMainAccount": {"items": []}} self.exchange._data_source._query_executor._order_history_responses.put_nowait(order_history_response) response = {"findOrderByMainAccount": None} @@ -478,17 +521,68 @@ def configure_order_not_found_error_order_status_response( def configure_partial_fill_trade_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: - raise NotImplementedError + order_fills_response = { + "listTradesByMainAccount": { + "items": [ + { + "m": self.exchange_trading_pair, + "p": str(self.expected_partial_fill_price), + "q": str(self.expected_partial_fill_amount), + "m_id": order.exchange_order_id, + "trade_id": self.expected_fill_trade_id, + "t": str(int(self.exchange.current_timestamp * 1e3)), + } + ] + } + } + self.exchange._data_source._query_executor._order_fills_responses.put_nowait(order_fills_response) + return "" def configure_erroneous_http_fill_trade_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: - raise NotImplementedError + error = { + "path": ["listTradesByMainAccount"], + "data": None, + "errorType": "DynamoDB:DynamoDbException", + "errorInfo": None, + "locations": [{"line": 2, "column": 3, "sourceName": None}], + "message": ( + "Invalid KeyConditionExpression: The BETWEEN operator requires upper bound to be greater than or" + " equal to lower bound; lower bound operand: AttributeValue: {N:1691691033195}, upper bound operand:" + " AttributeValue: {N:1691691023195} (Service: DynamoDb, Status Code: 400, Request ID:" + " F314JNSTC7U56DMFAFEPAGCM9VVV4KQNSO5AEMVJF66Q9ASUAAJG)" + ), + } + response = TransportQueryError(error) + mock_queue = AsyncMock() + mock_queue.get.side_effect = partial(self._callback_wrapper_with_response, callback=callback, response=response) + self.exchange._data_source._query_executor._order_fills_responses = mock_queue + return "" def configure_full_fill_trade_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: - raise NotImplementedError + order_fills_response = { + "listTradesByMainAccount": { + "items": [ + { + "m": self.exchange_trading_pair, + "p": str(order.price), + "q": str(order.amount), + "m_id": order.exchange_order_id, + "trade_id": self.expected_fill_trade_id, + "t": str(int(self.exchange.current_timestamp * 1e3)), + } + ] + } + } + self.exchange._data_source._query_executor._order_fills_responses.put_nowait(order_fills_response) + return "" + + def configure_no_fills_trade_response(self): + order_fills_response = {"listTradesByMainAccount": {"items": []}} + self.exchange._data_source._query_executor._order_fills_responses.put_nowait(order_fills_response) def configure_all_symbols_response( self, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None @@ -513,7 +607,7 @@ def configure_successful_creation_order_status_response( def configure_erroneous_creation_order_status_response( self, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: - creation_response = {"place_order": None} + creation_response = {"place_order": json.dumps({"is_success": False, "error": "some error"})} mock_queue = AsyncMock() mock_queue.get.side_effect = partial( self._callback_wrapper_with_response, callback=callback, response=creation_response @@ -522,82 +616,128 @@ def configure_erroneous_creation_order_status_response( return "" def order_event_for_new_order_websocket_update(self, order: InFlightOrder): - data = { - "type": "Order", - "snapshot_number": 50133, - "event_id": 4300054, - "client_order_id": "0x" + order.client_order_id.encode("utf-8").hex(), - "avg_filled_price": "0", - "fee": "0", - "filled_quantity": "0", - "status": "OPEN", - "id": order.exchange_order_id, - "user": "5EqHNNKJWA4U6dyZDvUSkKPQCt6PGgrAxiSBRvC6wqz2xKXU", - "main_account": "5C5ZpV7Hunb7yG2CwDtnhxaYc3aug4UTLxRvu6HERxJqrtJY", - "pair": {"base_asset": self.base_asset, "quote_asset": "1"}, - "side": "Bid" if order.trade_type == TradeType.BUY else "Ask", - "order_type": "MARKET" if order.order_type == OrderType.MARKET else "LIMIT", - "qty": str(order.amount), - "price": str(order.price), - "quote_order_qty": str(order.amount * order.price), - "timestamp": 1682480373, - "overall_unreserved_volume": "0", - } + data = self.build_order_event_websocket_update( + order=order, + filled_quantity=Decimal("0"), + filled_price=Decimal("0"), + fee=Decimal("0"), + status="OPEN", + ) + return data - return {"websocket_streams": {"data": json.dumps(data)}} + def order_event_for_partially_filled_websocket_update(self, order: InFlightOrder): + data = self.build_order_event_websocket_update( + order=order, + filled_quantity=self.expected_partial_fill_amount, + filled_price=self.expected_partial_fill_price, + fee=Decimal("0"), + status="OPEN", + ) + return data + + def order_event_for_partially_canceled_websocket_update(self, order: InFlightOrder): + data = self.build_order_event_websocket_update( + order=order, + filled_quantity=self.expected_partial_fill_amount, + filled_price=self.expected_partial_fill_price, + fee=Decimal("0"), + status="CANCELLED", + ) + return data def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): + data = self.build_order_event_websocket_update( + order=order, + filled_quantity=Decimal("0"), + filled_price=Decimal("0"), + fee=Decimal("0"), + status="CANCELLED", + ) + return data + + def order_event_for_full_fill_websocket_update(self, order: InFlightOrder): + data = self.build_order_event_websocket_update( + order=order, + filled_quantity=order.amount, + filled_price=order.price, + fee=Decimal("0"), + status="CLOSED", + ) + return data + + def build_order_event_websocket_update( + self, + order: InFlightOrder, + filled_quantity: Decimal, + filled_price: Decimal, + fee: Decimal, + status: str, + ): data = { "type": "Order", - "snapshot_number": 50133, - "event_id": 4300054, - "client_order_id": "0x" + order.client_order_id.encode("utf-8").hex(), - "avg_filled_price": "0", - "fee": "0", - "filled_quantity": "0", - "status": "CANCELLED", + "stid": 50133, + "client_order_id": order.client_order_id, + "avg_filled_price": str(filled_price), + "fee": str(fee), + "filled_quantity": str(filled_quantity), + "status": status, "id": order.exchange_order_id, - "user": "5EqHNNKJWA4U6dyZDvUSkKPQCt6PGgrAxiSBRvC6wqz2xKXU", - "main_account": "5C5ZpV7Hunb7yG2CwDtnhxaYc3aug4UTLxRvu6HERxJqrtJY", - "pair": {"base_asset": self.base_asset, "quote_asset": "1"}, + "user": "5EqHNNKJWA4U6dyZDvUSkKPQCt6PGgrAxiSBRvC6wqz2xKXU", # noqa: mock + "pair": {"base": {"asset": self.base_asset}, "quote": {"asset": "1"}}, "side": "Bid" if order.trade_type == TradeType.BUY else "Ask", "order_type": "MARKET" if order.order_type == OrderType.MARKET else "LIMIT", "qty": str(order.amount), "price": str(order.price), - "quote_order_qty": str(order.amount * order.price), "timestamp": 1682480373, - "overall_unreserved_volume": "0", } return {"websocket_streams": {"data": json.dumps(data)}} - def order_event_for_full_fill_websocket_update(self, order: InFlightOrder): + def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): + data = self.build_trade_event_websocket_update( + order=order, + filled_quantity=order.amount, + filled_price=order.price, + ) + return data + + def trade_event_for_partial_fill_websocket_update(self, order: InFlightOrder): + data = self.build_trade_event_websocket_update( + order=order, + filled_quantity=self.expected_partial_fill_amount, + filled_price=self.expected_partial_fill_price, + ) + return data + + def build_trade_event_websocket_update( + self, + order: InFlightOrder, + filled_quantity: Decimal, + filled_price: Decimal, + ) -> Dict[str, Any]: data = { - "type": "Order", - "snapshot_number": 50133, - "event_id": int(self.expected_fill_trade_id), - "client_order_id": "0x" + order.client_order_id.encode("utf-8").hex(), - "avg_filled_price": str(order.price), - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "filled_quantity": str(order.amount), - "status": "CLOSED", - "id": order.exchange_order_id, - "user": "5EqHNNKJWA4U6dyZDvUSkKPQCt6PGgrAxiSBRvC6wqz2xKXU", # noqa: mock - "main_account": "5C5ZpV7Hunb7yG2CwDtnhxaYc3aug4UTLxRvu6HERxJqrtJY", # noqa: mock - "pair": {"base_asset": self.base_asset, "quote_asset": "1"}, - "side": "Bid" if order.trade_type == TradeType.BUY else "Ask", - "order_type": "MARKET" if order.order_type == OrderType.MARKET else "LIMIT", - "qty": str(order.amount), - "price": str(order.price), - "quote_order_qty": str(order.amount * order.price), - "timestamp": 1682480373, - "overall_unreserved_volume": "0", + "type": "TradeFormat", + "stid": 50133, + "p": str(filled_price), + "q": str(filled_quantity), + "m": self.exchange_trading_pair, + "t": str(self.exchange.current_timestamp), + "cid": str(order.client_order_id), + "order_id": str(order.exchange_order_id), + "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", + "trade_id": self.expected_fill_trade_id, } return {"websocket_streams": {"data": json.dumps(data)}} - def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): - raise NotImplementedError + @aioresponses() + def test_check_network_success(self, mock_api): + all_assets_mock_response = self.all_assets_mock_response + self.exchange._data_source._query_executor._all_assets_responses.put_nowait(all_assets_mock_response) + + network_status = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + + self.assertEqual(NetworkStatus.CONNECTED, network_status) @aioresponses() def test_check_network_failure(self, mock_api): @@ -614,33 +754,22 @@ def test_check_network_raises_cancel_exception(self, mock_api): mock_queue.get.side_effect = asyncio.CancelledError self.exchange._data_source._query_executor._all_assets_responses = mock_queue - self.assertRaises( - asyncio.CancelledError, self.async_run_with_timeout, self.exchange.check_network(), 2 - ) - - @aioresponses() - def test_check_network_success(self, mock_api): - all_assets_mock_response = self.all_assets_mock_response - self.exchange._data_source._query_executor._all_assets_responses.put_nowait(all_assets_mock_response) - - network_status = self.async_run_with_timeout(coroutine=self.exchange.check_network()) - - self.assertEqual(NetworkStatus.CONNECTED, network_status) + self.assertRaises(asyncio.CancelledError, self.async_run_with_timeout, self.exchange.check_network()) @aioresponses() - def test_all_trading_pairs_does_not_raise_exception(self, mock_api): - self.exchange._set_trading_pair_symbol_map(None) - queue_mock = AsyncMock() - queue_mock.get.side_effect = Exception - self.exchange._data_source._query_executor._all_assets_responses = queue_mock + def test_get_last_trade_prices(self, mock_api): + response = self.latest_prices_request_mock_response + self.exchange._data_source._query_executor._recent_trades_responses.put_nowait(response) - result: List[str] = self.async_run_with_timeout(self.exchange.all_trading_pairs()) + latest_prices: Dict[str, float] = self.async_run_with_timeout( + self.exchange.get_last_traded_prices(trading_pairs=[self.trading_pair]) + ) - self.assertEqual(0, len(result)) + self.assertEqual(1, len(latest_prices)) + self.assertEqual(self.expected_latest_price, latest_prices[self.trading_pair]) @aioresponses() def test_invalid_trading_pair_not_in_all_trading_pairs(self, mock_api): - self.exchange._set_trading_pair_symbol_map(None) all_assets_mock_response = self.all_assets_mock_response self.exchange._data_source._query_executor._all_assets_responses.put_nowait(all_assets_mock_response) invalid_pair, response = self.all_symbols_including_invalid_pair_mock_response @@ -651,16 +780,16 @@ def test_invalid_trading_pair_not_in_all_trading_pairs(self, mock_api): self.assertNotIn(invalid_pair, all_trading_pairs) @aioresponses() - def test_get_last_trade_prices(self, mock_api): - response = self.latest_prices_request_mock_response - self.exchange._data_source._query_executor._recent_trades_responses.put_nowait(response) + def test_all_trading_pairs_does_not_raise_exception(self, mock_api): + self.exchange._set_trading_pair_symbol_map(None) + self.exchange._data_source._assets_map = None + queue_mock = AsyncMock() + queue_mock.get.side_effect = Exception + self.exchange._data_source._query_executor._all_assets_responses = queue_mock - latest_prices: Dict[str, float] = self.async_run_with_timeout( - self.exchange.get_last_traded_prices(trading_pairs=[self.trading_pair]) - ) + result: List[str] = self.async_run_with_timeout(self.exchange.all_trading_pairs()) - self.assertEqual(1, len(latest_prices)) - self.assertEqual(self.expected_latest_price, latest_prices[self.trading_pair]) + self.assertEqual(0, len(result)) @aioresponses() def test_create_buy_limit_order_successfully(self, mock_api): @@ -826,6 +955,48 @@ def test_cancel_order_successfully(self, mock_api): self.assertIn(order.client_order_id, self.exchange.in_flight_orders) self.assertTrue(order.is_pending_cancel_confirmation) + @aioresponses() + @patch("hummingbot.connector.exchange.polkadex.polkadex_data_source.PolkadexDataSource._build_substrate_interface") + @patch("hummingbot.connector.exchange.polkadex.polkadex_data_source.Keypair.sign") + def test_cancel_order_retries_on_substrate_broken_pipe( + self, mock_api: aioresponses, sign_mock: MagicMock, _: MagicMock + ): + sign_mock.hex.return_value = "0x1234adf" + request_sent_event = asyncio.Event() + self.exchange._set_current_timestamp(1640780000) + + self.exchange.start_tracking_order( + order_id=self.client_order_id_prefix + "1", + exchange_order_id=self.exchange_order_id_prefix + "1", + trading_pair=self.trading_pair, + trade_type=TradeType.BUY, + price=Decimal("10000"), + amount=Decimal("100"), + order_type=OrderType.LIMIT, + ) + + self.assertIn(self.client_order_id_prefix + "1", self.exchange.in_flight_orders) + order = self.exchange.in_flight_orders[self.client_order_id_prefix + "1"] + + create_scale_object_mock = MagicMock( + "substrateinterface.base.SubstrateInterface.create_scale_object", autospec=True + ) + self.exchange._data_source._substrate_interface.create_scale_object.return_value = create_scale_object_mock + create_scale_object_mock.encode.side_effect = [ + BrokenPipeError, + "0x1b99cba5555ad0ba890756fe16e499cb884b46a165b89bdce77ee8913b55fff1", # noqa: mock + ] + self.configure_successful_cancelation_response( + order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() + ) + + self.exchange.cancel(trading_pair=order.trading_pair, client_order_id=order.client_order_id) + self.async_run_with_timeout(request_sent_event.wait()) + + self.assertIn(order.client_order_id, self.exchange.in_flight_orders) + self.assertTrue(order.is_pending_cancel_confirmation) + self.assertTrue(self.is_logged(log_level="ERROR", message="Rebuilding the substrate interface.")) + @aioresponses() def test_cancel_order_raises_failure_event_when_request_fails(self, mock_api): request_sent_event = asyncio.Event() @@ -891,6 +1062,37 @@ def test_cancel_order_not_found_in_the_exchange(self, mock_api): self.assertIn(order.client_order_id, self.exchange._order_tracker.all_updatable_orders) self.assertEqual(1, self.exchange._order_tracker._order_not_found_records[order.client_order_id]) + @aioresponses() + def test_cancel_order_no_longer_active(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + request_sent_event = asyncio.Event() + + self.exchange.start_tracking_order( + order_id=self.client_order_id_prefix + "1", + exchange_order_id=str(self.expected_exchange_order_id), + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + price=Decimal("10000"), + amount=Decimal("1"), + ) + + self.assertIn(self.client_order_id_prefix + "1", self.exchange.in_flight_orders) + order = self.exchange.in_flight_orders[self.client_order_id_prefix + "1"] + + self.configure_order_not_active_error_cancelation_response( + order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() + ) + + self.exchange.cancel(trading_pair=self.trading_pair, client_order_id=self.client_order_id_prefix + "1") + self.async_run_with_timeout(request_sent_event.wait()) + + self.assertTrue(order.is_done) + self.assertFalse(order.is_failure) + self.assertTrue(order.is_cancelled) + + self.assertNotIn(order.client_order_id, self.exchange._order_tracker.all_updatable_orders) + @aioresponses() def test_update_balances(self, mock_api): response = self.balance_request_mock_response_for_base_and_quote @@ -919,58 +1121,6 @@ def test_update_balances(self, mock_api): self.assertEqual(Decimal("10"), available_balances[self.base_asset]) self.assertEqual(Decimal("15"), total_balances[self.base_asset]) - @aioresponses() - def test_update_order_status_when_filled(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - request_sent_event = asyncio.Event() - - self.exchange.start_tracking_order( - order_id=self.client_order_id_prefix + "1", - exchange_order_id=str(self.expected_exchange_order_id), - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal("10000"), - amount=Decimal("1"), - ) - order: InFlightOrder = self.exchange.in_flight_orders[self.client_order_id_prefix + "1"] - - # to allow the ClientOrderTracker to process the last status update - order.completely_filled_event.set() - - self.configure_completely_filled_order_status_response( - order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() - ) - - self.async_run_with_timeout(self.exchange._update_order_status()) - # Execute one more synchronization to ensure the async task that processes the update is finished - self.async_run_with_timeout(request_sent_event.wait()) - - self.async_run_with_timeout(order.wait_until_completely_filled()) - self.assertTrue(order.is_done) - - buy_event: BuyOrderCompletedEvent = self.buy_order_completed_logger.event_log[0] - self.assertEqual(self.exchange.current_timestamp, buy_event.timestamp) - self.assertEqual(order.client_order_id, buy_event.order_id) - self.assertEqual(order.base_asset, buy_event.base_asset) - self.assertEqual(order.quote_asset, buy_event.quote_asset) - self.assertEqual( - order.amount if self.is_order_fill_http_update_included_in_status_update else Decimal("0"), - buy_event.base_asset_amount, - ) - self.assertEqual( - order.amount * order.price - if self.is_order_fill_http_update_included_in_status_update - else Decimal("0"), - buy_event.quote_asset_amount, - ) - self.assertEqual(order.order_type, buy_event.order_type) - self.assertEqual(order.exchange_order_id, buy_event.exchange_order_id) - self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) - self.assertTrue( - self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.") - ) - @aioresponses() def test_update_order_status_when_request_fails_marks_order_as_not_found(self, mock_api): self.exchange._set_current_timestamp(1640780000) @@ -998,30 +1148,6 @@ def test_update_order_status_when_request_fails_marks_order_as_not_found(self, m self.assertEqual(1, self.exchange._order_tracker._order_not_found_records[order.client_order_id]) - @aioresponses() - def test_update_order_status_when_order_has_not_changed_and_one_partial_fill(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - - self.exchange.start_tracking_order( - order_id=self.client_order_id_prefix + "1", - exchange_order_id=str(self.expected_exchange_order_id), - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal("10000"), - amount=Decimal("1"), - ) - order: InFlightOrder = self.exchange.in_flight_orders[self.client_order_id_prefix + "1"] - - self.configure_partially_filled_order_status_response(order=order, mock_api=mock_api) - - self.assertTrue(order.is_open) - - self.async_run_with_timeout(self.exchange._update_order_status()) - - self.assertTrue(order.is_open) - self.assertEqual(OrderState.PARTIALLY_FILLED, order.current_state) - @aioresponses() def test_cancel_lost_order_successfully(self, mock_api): request_sent_event = asyncio.Event() @@ -1059,7 +1185,6 @@ def test_cancel_lost_order_successfully(self, mock_api): @aioresponses() def test_cancel_lost_order_raises_failure_event_when_request_fails(self, mock_api): - request_sent_event = asyncio.Event() self.exchange._set_current_timestamp(1640780000) self.exchange.start_tracking_order( @@ -1081,13 +1206,9 @@ def test_cancel_lost_order_raises_failure_event_when_request_fails(self, mock_ap self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) - self.configure_erroneous_cancelation_response( - order=order, - mock_api=mock_api, - callback=lambda *args, **kwargs: request_sent_event.set()) + self.exchange._data_source._query_executor._cancel_order_responses.put_nowait({}) self.async_run_with_timeout(self.exchange._cancel_lost_orders()) - self.async_run_with_timeout(request_sent_event.wait()) self.assertIn(order.client_order_id, self.exchange._order_tracker.lost_orders) self.assertEquals(0, len(self.order_cancelled_logger.event_log)) @@ -1152,9 +1273,12 @@ def test_lost_order_user_stream_full_fill_events_are_processed(self, mock_api): self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) order_event = self.order_event_for_full_fill_websocket_update(order=order) + trade_event = self.trade_event_for_full_fill_websocket_update(order=order) + self.reset_log_event() self.exchange._data_source._process_private_event(event=order_event) + self.exchange._data_source._process_private_event(event=trade_event) self.async_run_with_timeout(self.wait_for_a_log()) # Execute one more synchronization to ensure the async task that processes the update is finished @@ -1271,9 +1395,11 @@ def test_user_stream_update_for_order_full_fill(self, mock_api): order = self.exchange.in_flight_orders[self.client_order_id_prefix + "1"] order_event = self.order_event_for_full_fill_websocket_update(order=order) + trade_event = self.trade_event_for_full_fill_websocket_update(order=order) self.reset_log_event() self.exchange._data_source._process_private_event(event=order_event) + self.exchange._data_source._process_private_event(event=trade_event) self.async_run_with_timeout(self.wait_for_a_log()) # Execute one more synchronization to ensure the async task that processes the update is finished @@ -1303,7 +1429,9 @@ def test_user_stream_update_for_order_full_fill(self, mock_api): self.assertTrue(order.is_filled) self.assertTrue(order.is_done) - self.assertTrue(self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.")) + self.assertTrue( + self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.") + ) def test_user_stream_logs_errors(self): # This test does not apply to Polkadex because it handles private events in its own data source @@ -1336,25 +1464,36 @@ def test_lost_order_included_in_order_fills_update_and_not_in_order_status_updat self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) - order.completely_filled_event.set() - request_sent_event.set() + self.configure_full_fill_trade_response( + order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() + ) self.async_run_with_timeout(self.exchange._update_order_status()) - # Execute one more synchronization to ensure the async task that processes the update is finished - self.async_run_with_timeout(request_sent_event.wait()) self.async_run_with_timeout(order.wait_until_completely_filled()) self.assertTrue(order.is_done) self.assertTrue(order.is_failure) + fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] + self.assertEqual(self.exchange.current_timestamp, fill_event.timestamp) + self.assertEqual(order.client_order_id, fill_event.order_id) + self.assertEqual(order.trading_pair, fill_event.trading_pair) + self.assertEqual(order.trade_type, fill_event.trade_type) + self.assertEqual(order.order_type, fill_event.order_type) + self.assertEqual(order.price, fill_event.price) + self.assertEqual(order.amount, fill_event.amount) + self.assertEqual(self.expected_fill_fee, fill_event.trade_fee) + self.assertEqual(0, len(self.buy_order_completed_logger.event_log)) self.assertIn(order.client_order_id, self.exchange._order_tracker.all_fillable_orders) - self.assertFalse( - self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.") - ) + self.assertFalse(self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.")) request_sent_event.clear() + self.configure_full_fill_trade_response( + order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() + ) + self.configure_completely_filled_order_status_response( order=order, mock_api=mock_api, callback=lambda *args, **kwargs: request_sent_event.set() ) @@ -1366,11 +1505,11 @@ def test_lost_order_included_in_order_fills_update_and_not_in_order_status_updat self.assertTrue(order.is_done) self.assertTrue(order.is_failure) + if self.is_order_fill_http_update_included_in_status_update: + self.assertEqual(1, len(self.order_filled_logger.event_log)) self.assertEqual(0, len(self.buy_order_completed_logger.event_log)) self.assertNotIn(order.client_order_id, self.exchange._order_tracker.all_fillable_orders) - self.assertFalse( - self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.") - ) + self.assertFalse(self.is_logged("INFO", f"BUY order {order.client_order_id} completely filled.")) def test_initial_status_dict(self): self.exchange._set_trading_pair_symbol_map(None) @@ -1466,7 +1605,52 @@ def _configure_balance_response( def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return {"cancel_order": True} + def _all_trading_pairs_mock_response(self, orders_count: int, symbol: str) -> Any: + return { + "listOpenOrdersByMainAccount": { + "items": [ + { + "afp": "0", + "cid": f"0x48424f544250584354356663383135646636666166313531306165623366376{i}", + "fee": "0", + "fq": "0", + "id": f"0x541a3a1be1ad69cc0d325103ca54e4e12c8035d9474a96539af3323cae681fa{i}", + "m": symbol, + "ot": "LIMIT", + "p": f"1.51{i}", + "q": f"0.06{i}", + "s": "Bid", + "st": "OPEN", + "t": self.exchange.current_timestamp, + } + for i in range(orders_count) + ], + }, + } + def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: + return {"findOrderByMainAccount": self._orders_status_response(order=order)} + + def _orders_status_response(self, order: InFlightOrder) -> Any: + return { + "afp": "0", + "cid": "0x" + order.client_order_id.encode("utf-8").hex(), + "fee": "0", + "fq": "0", + "id": order.exchange_order_id, + "isReverted": False, + "m": self.exchange_trading_pair, + "ot": "MARKET" if order.order_type == OrderType.MARKET else "LIMIT", + "p": str(order.price), + "q": str(order.amount), + "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", + "sid": 1, + "st": "OPEN", + "t": 160001112.223, + "u": "", + } + + def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { "findOrderByMainAccount": { "afp": "0", @@ -1481,19 +1665,19 @@ def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: "q": str(order.amount), "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", "sid": 1, - "st": "OPEN", + "st": "CANCELLED", "t": 160001112.223, "u": "", } } - def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: + def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: return { "findOrderByMainAccount": { - "afp": "0", + "afp": str(order.price), "cid": "0x" + order.client_order_id.encode("utf-8").hex(), - "fee": "0", - "fq": "0", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "fq": str(order.amount), "id": order.exchange_order_id, "isReverted": False, "m": self.exchange_trading_pair, @@ -1501,20 +1685,20 @@ def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> "p": str(order.price), "q": str(order.amount), "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", - "sid": 1, - "st": "CANCELLED", + "sid": int(self.expected_fill_trade_id), + "st": "CLOSED", "t": 160001112.223, "u": "", } } - def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: + def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: return { "findOrderByMainAccount": { - "afp": str(order.price), + "afp": str(self.expected_partial_fill_price), "cid": "0x" + order.client_order_id.encode("utf-8").hex(), - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "fq": str(order.amount), + "fee": str(self.expected_partial_fill_fee.flat_fees[0].amount), + "fq": str(self.expected_partial_fill_amount), "id": order.exchange_order_id, "isReverted": False, "m": self.exchange_trading_pair, @@ -1523,13 +1707,13 @@ def _order_status_request_completely_filled_mock_response(self, order: InFlightO "q": str(order.amount), "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", "sid": int(self.expected_fill_trade_id), - "st": "CLOSED", + "st": "OPEN", "t": 160001112.223, "u": "", } } - def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: + def _order_status_request_partially_canceled_mock_response(self, order: InFlightOrder) -> Any: return { "findOrderByMainAccount": { "afp": str(self.expected_partial_fill_price), @@ -1544,7 +1728,7 @@ def _order_status_request_partially_filled_mock_response(self, order: InFlightOr "q": str(order.amount), "s": "Bid" if order.trade_type == TradeType.BUY else "Ask", "sid": int(self.expected_fill_trade_id), - "st": "OPEN", + "st": "CANCELLED", "t": 160001112.223, "u": "", } diff --git a/test/hummingbot/connector/gateway/clob_spot/data_sources/injective/test_injective_utils.py b/test/hummingbot/connector/gateway/clob_spot/data_sources/injective/test_injective_utils.py new file mode 100644 index 0000000000..44e189db2d --- /dev/null +++ b/test/hummingbot/connector/gateway/clob_spot/data_sources/injective/test_injective_utils.py @@ -0,0 +1,48 @@ +from decimal import Decimal +from unittest import TestCase + +from pyinjective.constant import Denom + +from hummingbot.connector.gateway.clob_spot.data_sources.injective.injective_utils import ( + derivative_price_to_backend, + derivative_quantity_to_backend, + floor_to, +) + + +class InjectiveUtilsTests(TestCase): + + def test_floor_to_utility_method(self): + original_value = Decimal("123.0123456789") + + result = floor_to(value=original_value, target=Decimal("0.001")) + self.assertEqual(Decimal("123.012"), result) + + result = floor_to(value=original_value, target=Decimal("1")) + self.assertEqual(Decimal("123"), result) + + def test_derivative_quantity_to_backend_utility_method(self): + denom = Denom( + description="Fixed denom", + base=2, + quote=6, + min_price_tick_size=1000, + min_quantity_tick_size=100, + ) + + backend_quantity = derivative_quantity_to_backend(quantity=Decimal("1"), denom=denom) + + self.assertEqual(100000000000000000000, backend_quantity) + + def test_derivative_price_to_backend_utility_method(self): + denom = Denom( + description="Fixed denom", + base=2, + quote=6, + min_price_tick_size=1000, + min_quantity_tick_size=100, + ) + + backend_quantity = derivative_price_to_backend(price=Decimal("123.45"), denom=denom) + + self.assertEqual(123450000000000000000000000, backend_quantity) diff --git a/test/hummingbot/connector/gateway/clob_spot/data_sources/kujira/__init__.py b/test/hummingbot/connector/gateway/clob_spot/data_sources/kujira/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/connector/gateway/clob_spot/data_sources/kujira/test_kujira_api_data_source.py b/test/hummingbot/connector/gateway/clob_spot/data_sources/kujira/test_kujira_api_data_source.py new file mode 100644 index 0000000000..54ed60a918 --- /dev/null +++ b/test/hummingbot/connector/gateway/clob_spot/data_sources/kujira/test_kujira_api_data_source.py @@ -0,0 +1,722 @@ +import asyncio +from typing import Any, Dict, List, Union +from unittest.mock import patch + +from _decimal import Decimal +from bidict import bidict +from dotmap import DotMap + +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_api_data_source import KujiraAPIDataSource +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_helpers import ( + convert_hb_trading_pair_to_market_name, + convert_market_name_to_hb_trading_pair, + generate_hash, +) +from hummingbot.connector.gateway.clob_spot.data_sources.kujira.kujira_types import ( + OrderSide as KujiraOrderSide, + OrderStatus as KujiraOrderStatus, + OrderType as KujiraOrderType, +) +from hummingbot.connector.gateway.gateway_in_flight_order import GatewayInFlightOrder +from hummingbot.connector.test_support.gateway_clob_api_data_source_test import AbstractGatewayCLOBAPIDataSourceTests +from hummingbot.connector.trading_rule import TradingRule +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.in_flight_order import OrderState, OrderUpdate, TradeUpdate +from hummingbot.core.data_type.order_book_message import OrderBookMessage +from hummingbot.core.data_type.trade_fee import ( + DeductedFromReturnsTradeFee, + MakerTakerExchangeFeeRates, + TokenAmount, + TradeFeeBase, +) +from hummingbot.core.network_iterator import NetworkStatus + + +class KujiraAPIDataSourceTest(AbstractGatewayCLOBAPIDataSourceTests.GatewayCLOBAPIDataSourceTests): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.chain = "kujira" # noqa: mock + cls.network = "mainnet" + cls.base = "KUJI" # noqa: mock + cls.quote = "USK" + cls.trading_pair = combine_to_hb_trading_pair(base=cls.base, quote=cls.quote) + cls.owner_address = "kujira1yrensec9gzl7y3t3duz44efzgwj21b2arayr1w" # noqa: mock + + def setUp(self) -> None: + super().setUp() + + self.configure_asyncio_sleep() + self.data_source._gateway = self.gateway_instance_mock + self.configure_async_functions_with_decorator() + self.configure_get_market() + + def tearDown(self) -> None: + super().tearDown() + + @property + def expected_buy_client_order_id(self) -> str: + return "03719e91d18db65ec3bf5554d678e5b4" + + @property + def expected_sell_client_order_id(self) -> str: + return "02719e91d18db65ec3bf5554d678e5b2" + + @property + def expected_buy_exchange_order_id(self) -> str: + return "1" + + @property + def expected_sell_exchange_order_id(self) -> str: + return "2" + + @property + def exchange_base(self) -> str: + return self.base + + @property + def exchange_quote(self) -> str: + return self.quote + + @property + def expected_quote_decimals(self) -> int: + return 6 + + @property + def expected_base_decimals(self) -> int: + return 6 + + @property + def expected_maker_taker_fee_rates(self) -> MakerTakerExchangeFeeRates: + return MakerTakerExchangeFeeRates( + maker=Decimal("0.075"), + taker=Decimal("0.15"), + maker_flat_fees=[], + taker_flat_fees=[], + ) + + @property + def expected_min_price_increment(self): + return Decimal("0.001") + + @property + def expected_last_traded_price(self) -> Decimal: + return Decimal("0.641") + + @property + def expected_base_total_balance(self) -> Decimal: + return Decimal("6.355439") + + @property + def expected_base_available_balance(self) -> Decimal: + return Decimal("6.355439") + + @property + def expected_quote_total_balance(self) -> Decimal: + return Decimal("3.522325") + + @property + def expected_quote_available_balance(self) -> Decimal: + return Decimal("3.522325") + + @property + def expected_fill_price(self) -> Decimal: + return Decimal("11") + + @property + def expected_fill_size(self) -> Decimal: + return Decimal("3") + + @property + def expected_fill_fee_amount(self) -> Decimal: + return Decimal("0.15") + + @property + def expected_fill_fee(self) -> TradeFeeBase: + return DeductedFromReturnsTradeFee( + flat_fees=[TokenAmount(token=self.expected_fill_fee_token, amount=self.expected_fill_fee_amount)] + ) + + def build_api_data_source(self, with_api_key: bool = True) -> Any: + connector_spec = { + "chain": self.chain, + "network": self.network, + "wallet_address": self.owner_address, + } + + data_source = KujiraAPIDataSource( + trading_pairs=[self.trading_pair], + connector_spec=connector_spec, + client_config_map=self.client_config_map, + ) + + return data_source + + @staticmethod + def configure_asyncio_sleep(): + async def sleep(*_args, **_kwargs): + pass + + patch.object(asyncio, "sleep", new_callable=sleep) + + def configure_async_functions_with_decorator(self): + def wrapper(object, function): + async def closure(*args, **kwargs): + return await function(object, *args, **kwargs) + + return closure + + self.data_source._gateway_ping_gateway = wrapper(self.data_source, self.data_source._gateway_ping_gateway.original) + self.data_source._gateway_get_clob_markets = wrapper(self.data_source, self.data_source._gateway_get_clob_markets.original) + self.data_source._gateway_get_clob_orderbook_snapshot = wrapper(self.data_source, self.data_source._gateway_get_clob_orderbook_snapshot.original) + self.data_source._gateway_get_clob_ticker = wrapper(self.data_source, self.data_source._gateway_get_clob_ticker.original) + self.data_source._gateway_get_balances = wrapper(self.data_source, self.data_source._gateway_get_balances.original) + self.data_source._gateway_clob_place_order = wrapper(self.data_source, self.data_source._gateway_clob_place_order.original) + self.data_source._gateway_clob_cancel_order = wrapper(self.data_source, self.data_source._gateway_clob_cancel_order.original) + self.data_source._gateway_clob_batch_order_modify = wrapper(self.data_source, self.data_source._gateway_clob_batch_order_modify.original) + self.data_source._gateway_get_clob_order_status_updates = wrapper(self.data_source, self.data_source._gateway_get_clob_order_status_updates.original) + + @patch("hummingbot.core.gateway.gateway_http_client.GatewayHttpClient.get_clob_markets") + def configure_get_market(self, *_args): + self.data_source._gateway.get_clob_markets.return_value = self.configure_gateway_get_clob_markets_response() + + def configure_place_order_response( + self, + timestamp: float, + transaction_hash: str, + exchange_order_id: str, + trade_type: TradeType, + price: Decimal, + size: Decimal, + ): + super().configure_place_order_response( + timestamp, + transaction_hash, + exchange_order_id, + trade_type, + price, + size, + ) + self.gateway_instance_mock.clob_place_order.return_value["id"] = "1" + + def configure_place_order_failure_response(self): + super().configure_place_order_failure_response() + self.gateway_instance_mock.clob_place_order.return_value["id"] = "1" + + def configure_batch_order_create_response( + self, + timestamp: float, + transaction_hash: str, + created_orders: List[GatewayInFlightOrder], + ): + super().configure_batch_order_create_response( + timestamp=self.initial_timestamp, + transaction_hash=self.expected_transaction_hash, + created_orders=created_orders, + ) + self.gateway_instance_mock.clob_batch_order_modify.return_value["ids"] = ["1", "2"] + + def get_trading_pairs_info_response(self) -> List[Dict[str, Any]]: + response = self.configure_gateway_get_clob_markets_response() + + market = response.markets[list(response.markets.keys())[0]] + + market_name = convert_market_name_to_hb_trading_pair(market.name) + + return [{"market_name": market_name, "market": market}] + + def get_order_status_response( + self, + timestamp: float, + trading_pair: str, + exchange_order_id: str, + client_order_id: str, + status: OrderState + ) -> List[Dict[str, Any]]: + return [DotMap({ + "id": exchange_order_id, + "orderHash": "", + "marketId": "kujira193dzcmy7lwuj4eda3zpwwt9ejal00xva0vawcvhgsyyp5cfh6jyq66wfrf", # noqa: mock + "active": "", + "subaccountId": "", # noqa: mock + "executionType": "", + "orderType": "LIMIT", + "price": "0.616", + "triggerPrice": "", + "quantity": "0.24777", + "filledQuantity": "", + "state": KujiraOrderStatus.from_hummingbot(status).name, + "createdAt": timestamp, + "updatedAt": "", + "direction": "BUY" + })] + + def get_clob_ticker_response( + self, + trading_pair: str, + last_traded_price: Decimal + ) -> Dict[str, Any]: + market = ( + self.configure_gateway_get_clob_markets_response() + ).markets[trading_pair] + + return { + "KUJI-USK": { # noqa: mock + "market": market, + "ticker": { + "price": "0.641" + }, + "price": "0.641", + "timestamp": 1694631135095 + } + } + + def configure_account_balances_response( + self, + base_total_balance: Decimal, + base_available_balance: Decimal, + quote_total_balance: Decimal, + quote_available_balance: Decimal + ): + self.gateway_instance_mock.get_balances.return_value = self.configure_gateway_get_balances_response() + + def configure_empty_order_fills_response(self): + pass + + def configure_trade_fill_response( + self, + timestamp: float, + exchange_order_id: str, + price: Decimal, + size: Decimal, + fee: TradeFeeBase, trade_id: Union[str, int], is_taker: bool + ): + pass + + @staticmethod + def configure_gateway_get_clob_markets_response(): + return DotMap({ + "network": "mainnet", + "timestamp": 1694561843115, + "latency": 0.001, + "markets": { + "KUJI-USK": { # noqa: mock + "id": "kujira193dzcmy7lwuj4eda3zpwwt9ejal00xva0vawcvhgsyyp5cfh6jyq66wfrf", # noqa: mock + "name": "KUJI/USK", # noqa: mock + "baseToken": { + "id": "ukuji", # noqa: mock + "name": "KUJI", # noqa: mock + "symbol": "KUJI", # noqa: mock + "decimals": 6 + }, + "quoteToken": { + "id": "factory/kujira1qk00h5atutpsv900x202pxx42npjr9thg58dnqpa72f2p7m2luase444a7/uusk", + # noqa: mock + "name": "USK", + "symbol": "USK", + "decimals": 6 + }, + "precision": 3, + "minimumOrderSize": "0.001", + "minimumPriceIncrement": "0.001", + "minimumBaseAmountIncrement": "0.001", + "minimumQuoteAmountIncrement": "0.001", + "fees": { + "maker": "0.075", + "taker": "0.15", + "serviceProvider": "0" + }, + "deprecated": False, + "connectorMarket": { + "address": "kujira193dzcmy7lwuj4eda3zpwwt9ejal00xva0vawcvhgsyyp5cfh6jyq66wfrf", # noqa: mock + "denoms": [ # noqa: mock + { + "reference": "ukuji", # noqa: mock + "decimals": 6, + "symbol": "KUJI" # noqa: mock + }, + { + "reference": "factory/kujira1qk00h5atutpsv900x202pxx42npjr9thg58dnqpa72f2p7m2luase444a7/uusk", + # noqa: mock + "decimals": 6, + "symbol": "USK" + } + ], + "precision": { + "decimal_places": 3 + }, + "decimalDelta": 0, + "multiswap": True, # noqa: mock + "pool": "kujira1g9xcvvh48jlckgzw8ajl6dkvhsuqgsx2g8u3v0a6fx69h7f8hffqaqu36t", # noqa: mock + "calc": "kujira1e6fjnq7q20sh9cca76wdkfg69esha5zn53jjewrtjgm4nktk824stzyysu" # noqa: mock + } + } + } + }, _dynamic=False) + + def configure_gateway_get_balances_response(self): + return { + "balances": { + "USK": "3.522325", + "axlUSDC": "1.999921", + "KUJI": "6.355439" + } + } + + def exchange_symbol_for_tokens( + self, + base_token: str, + quote_token: str + ) -> str: + return f"{base_token}-{quote_token}" + + @patch("hummingbot.core.gateway.gateway_http_client.GatewayHttpClient.ping_gateway") + def test_gateway_ping_gateway(self, *_args): + self.data_source._gateway.ping_gateway.return_value = True + + result = self.async_run_with_timeout( + coro=self.data_source._gateway_ping_gateway() + ) + + expected = True + + self.assertEqual(expected, result) + + @patch("hummingbot.core.gateway.gateway_http_client.GatewayHttpClient.ping_gateway") + def test_check_network_status_with_gateway_connected(self, *_args): + self.data_source._gateway.ping_gateway.return_value = True + + result = self.async_run_with_timeout( + coro=self.data_source.check_network_status() + ) + + expected = NetworkStatus.CONNECTED + + self.assertEqual(expected, result) + + @patch("hummingbot.core.gateway.gateway_http_client.GatewayHttpClient.ping_gateway") + def test_check_network_status_with_gateway_not_connected(self, *_args): + self.data_source._gateway.ping_gateway.return_value = False + + result = self.async_run_with_timeout( + coro=self.data_source.check_network_status() + ) + + expected = NetworkStatus.NOT_CONNECTED + + self.assertEqual(expected, result) + + @patch("hummingbot.core.gateway.gateway_http_client.GatewayHttpClient.ping_gateway") + def test_check_network_status_with_gateway_exception(self, *_args): + self.configure_asyncio_sleep() + self.data_source._gateway.ping_gateway.side_effect = RuntimeError("Unknown error") + + result = self.async_run_with_timeout( + coro=self.data_source.check_network_status() + ) + + expected = NetworkStatus.NOT_CONNECTED + + self.assertEqual(expected, result) + + def test_batch_order_cancel(self): + super().test_batch_order_cancel() + + def test_batch_order_create(self): + super().test_batch_order_create() + + def test_cancel_order(self): + super().test_cancel_order() + + def test_cancel_order_transaction_fails(self): + order = GatewayInFlightOrder( + client_order_id=self.expected_buy_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + price=self.expected_buy_order_price, + amount=self.expected_buy_order_size, + creation_timestamp=self.initial_timestamp, + exchange_order_id=self.expected_buy_exchange_order_id, + creation_transaction_hash="someCreationHash", + ) + self.data_source.gateway_order_tracker.start_tracking_order(order=order) + self.configure_cancel_order_failure_response() + + result = self.async_run_with_timeout(coro=self.data_source.cancel_order(order=order)) + + self.assertEqual(False, result[0]) + self.assertEqual(DotMap({}), result[1]) + + def test_check_network_status(self): + super().test_check_network_status() + + def test_delivers_balance_events(self): + super().test_delivers_balance_events() + + def test_delivers_order_book_snapshot_events(self): + pass + + def test_get_account_balances(self): + super().test_get_account_balances() + + def test_get_all_order_fills(self): + asyncio.get_event_loop().run_until_complete( + self.data_source._update_markets() + ) + creation_transaction_hash = "0x7cb2eafc389349f86da901cdcbfd9119425a2ea84d61c17b6ded778b6fd2g81d" # noqa: mock + in_flight_order = GatewayInFlightOrder( + initial_state=OrderState.PENDING_CREATE, + client_order_id=self.expected_sell_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.SELL, + creation_timestamp=self.initial_timestamp - 10, + price=self.expected_sell_order_price, + amount=self.expected_sell_order_size, + exchange_order_id=self.expected_sell_exchange_order_id, + ) + self.data_source.gateway_order_tracker.active_orders[in_flight_order.client_order_id] = in_flight_order + self.enqueue_order_status_response( + timestamp=self.initial_timestamp + 1, + trading_pair=in_flight_order.trading_pair, + exchange_order_id=self.expected_buy_exchange_order_id, + client_order_id=in_flight_order.client_order_id, + status=OrderState.FILLED, + ) + + trade_updates: List[TradeUpdate] = self.async_run_with_timeout( + coro=self.data_source.get_all_order_fills(in_flight_order=in_flight_order), + ) + + self.assertEqual(1, len(trade_updates)) + + trade_update = trade_updates[0] + + self.assertIsNotNone(trade_update.trade_id) + self.assertEqual(self.expected_sell_client_order_id, trade_update.client_order_id) + self.assertEqual(self.expected_sell_exchange_order_id, trade_update.exchange_order_id) + self.assertEqual(self.trading_pair, trade_update.trading_pair) + self.assertLess(float(0), trade_update.fill_timestamp) + self.assertEqual(self.expected_fill_price, trade_update.fill_price) + self.assertEqual(self.expected_fill_size, trade_update.fill_base_amount) + self.assertEqual(self.expected_fill_size * self.expected_fill_price, trade_update.fill_quote_amount) + self.assertEqual(self.expected_fill_fee, trade_update.fee) + self.assertTrue(trade_update.is_taker) + + def test_get_all_order_fills_no_fills(self): + super().test_get_all_order_fills_no_fills() + + def test_get_last_traded_price(self): + self.configure_last_traded_price( + trading_pair=self.trading_pair, last_traded_price=self.expected_last_traded_price + ) + last_trade_price = self.async_run_with_timeout( + coro=self.data_source.get_last_traded_price(trading_pair=self.trading_pair) + ) + + self.assertEqual(self.expected_last_traded_price, last_trade_price) + + def test_get_order_book_snapshot(self): + self.configure_orderbook_snapshot( + timestamp=self.initial_timestamp, bids=[[9, 1], [8, 2]], asks=[[11, 3]] + ) + order_book_snapshot: OrderBookMessage = self.async_run_with_timeout( + coro=self.data_source.get_order_book_snapshot(trading_pair=self.trading_pair) + ) + + self.assertLess(float(0), order_book_snapshot.timestamp) + self.assertEqual(2, len(order_book_snapshot.bids)) + self.assertEqual(9, order_book_snapshot.bids[0].price) + self.assertEqual(1, order_book_snapshot.bids[0].amount) + self.assertEqual(1, len(order_book_snapshot.asks)) + self.assertEqual(11, order_book_snapshot.asks[0].price) + self.assertEqual(3, order_book_snapshot.asks[0].amount) + + def test_get_order_status_update(self): + creation_transaction_hash = "0x7cb2eafc389349f86da901cdcbfd9119425a2ea84d61c17b6ded778b6fd2g81d" # noqa: mock + in_flight_order = GatewayInFlightOrder( + client_order_id=self.expected_buy_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + creation_timestamp=self.initial_timestamp, + price=self.expected_buy_order_price, + amount=self.expected_buy_order_size, + creation_transaction_hash=creation_transaction_hash, + exchange_order_id=self.expected_buy_exchange_order_id, + ) + self.data_source.gateway_order_tracker.active_orders[in_flight_order.client_order_id] = in_flight_order + self.enqueue_order_status_response( + timestamp=self.initial_timestamp + 1, + trading_pair=in_flight_order.trading_pair, + exchange_order_id=self.expected_buy_exchange_order_id, + client_order_id=in_flight_order.client_order_id, + status=OrderState.PENDING_CREATE, + ) + + status_update: OrderUpdate = self.async_run_with_timeout( + coro=self.data_source.get_order_status_update(in_flight_order=in_flight_order) + ) + + self.assertEqual(self.trading_pair, status_update.trading_pair) + self.assertLess(self.initial_timestamp, status_update.update_timestamp) + self.assertEqual(OrderState.PENDING_CREATE, status_update.new_state) + self.assertEqual(in_flight_order.client_order_id, status_update.client_order_id) + self.assertEqual(self.expected_buy_exchange_order_id, status_update.exchange_order_id) + + def test_get_order_status_update_with_no_update(self): + creation_transaction_hash = "0x7cb2eafc389349f86da901cdcbfd9119425a2ea84d61c17b6ded778b6fd2g81d" # noqa: mock + in_flight_order = GatewayInFlightOrder( + client_order_id=self.expected_buy_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + creation_timestamp=self.initial_timestamp, + price=self.expected_buy_order_price, + amount=self.expected_buy_order_size, + creation_transaction_hash=creation_transaction_hash, + exchange_order_id=self.expected_buy_exchange_order_id, + ) + self.enqueue_order_status_response( + timestamp=self.initial_timestamp + 1, + trading_pair=in_flight_order.trading_pair, + exchange_order_id=self.expected_buy_exchange_order_id, + client_order_id=in_flight_order.client_order_id, + status=OrderState.PENDING_CREATE, + ) + + status_update: OrderUpdate = self.async_run_with_timeout( + coro=self.data_source.get_order_status_update(in_flight_order=in_flight_order) + ) + + self.assertEqual(self.trading_pair, status_update.trading_pair) + self.assertLess(self.initial_timestamp, status_update.update_timestamp) + self.assertEqual(OrderState.PENDING_CREATE, status_update.new_state) + self.assertEqual(in_flight_order.client_order_id, status_update.client_order_id) + self.assertEqual(self.expected_buy_exchange_order_id, status_update.exchange_order_id) + + def test_update_order_status(self): + creation_transaction_hash = "0x7cb2eafc389349f86da901cdcbfd9119425a2ea84d61c17b6ded778b6fd2g81d" # noqa: mock + in_flight_order = GatewayInFlightOrder( + client_order_id=self.expected_buy_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + creation_timestamp=self.initial_timestamp, + price=self.expected_buy_order_price, + amount=self.expected_buy_order_size, + creation_transaction_hash=creation_transaction_hash, + exchange_order_id=self.expected_buy_exchange_order_id, + ) + self.data_source.gateway_order_tracker.active_orders[in_flight_order.client_order_id] = in_flight_order + self.enqueue_order_status_response( + timestamp=self.initial_timestamp + 1, + trading_pair=in_flight_order.trading_pair, + exchange_order_id=self.expected_buy_exchange_order_id, + client_order_id=in_flight_order.client_order_id, + status=OrderState.PENDING_CREATE, + ) + + self.async_run_with_timeout( + coro=self.data_source._update_order_status() + ) + + def test_get_symbol_map(self): + symbol_map = self.async_run_with_timeout(coro=self.data_source.get_symbol_map()) + + self.assertIsInstance(symbol_map, bidict) + self.assertEqual(1, len(symbol_map)) + self.assertIn(self.exchange_trading_pair, symbol_map.inverse) + + def test_get_trading_fees(self): + super().test_get_trading_fees() + + def test_get_trading_rules(self): + trading_rules = self.async_run_with_timeout(coro=self.data_source.get_trading_rules()) + + self.assertEqual(1, len(trading_rules)) + self.assertIn(self.trading_pair, trading_rules) + + trading_rule: TradingRule = trading_rules[self.trading_pair] + + self.assertEqual(self.trading_pair, trading_rule.trading_pair) + self.assertEqual(self.expected_min_price_increment, trading_rule.min_price_increment) + + def test_maximum_delay_between_requests_for_snapshot_events(self): + pass + + def test_minimum_delay_between_requests_for_snapshot_events(self): + pass + + def test_place_order(self): + super().test_place_order() + + def test_place_order_transaction_fails(self): + self.configure_place_order_failure_response() + + order = GatewayInFlightOrder( + client_order_id=self.expected_buy_client_order_id, + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + creation_timestamp=self.initial_timestamp, + price=self.expected_buy_order_price, + amount=self.expected_buy_order_size, + ) + + with self.assertRaises(Exception): + self.async_run_with_timeout( + coro=self.data_source.place_order(order=order) + ) + + def test_generate_hash(self): + actual = generate_hash("test") + + self.assertIsNotNone(actual) + + def test_convert_hb_trading_pair_to_market_name(self): + expected = "KUJI/USK" + + actual = convert_hb_trading_pair_to_market_name("KUJI-USK") + + self.assertEqual(expected, actual) + + def test_order_status_methods(self): + for item in KujiraOrderStatus: + if item == KujiraOrderStatus.UNKNOWN: + continue + + hummingbot_status = KujiraOrderStatus.to_hummingbot(item) + kujira_status = KujiraOrderStatus.from_hummingbot(hummingbot_status) + kujira_status_from_name = KujiraOrderStatus.from_name(kujira_status.name) + + self.assertEqual(item, kujira_status) + self.assertEqual(item, kujira_status_from_name) + + def test_order_sides(self): + for item in KujiraOrderSide: + hummingbot_side = KujiraOrderSide.to_hummingbot(item) + kujira_side = KujiraOrderSide.from_hummingbot(hummingbot_side) + kujira_side_from_name = KujiraOrderSide.from_name(kujira_side.name) + + self.assertEqual(item, kujira_side) + self.assertEqual(item, kujira_side_from_name) + + def test_order_types(self): + for item in KujiraOrderType: + if item != KujiraOrderType.MARKET: + hummingbot_type = KujiraOrderType.to_hummingbot(item) + kujira_type = KujiraOrderType.from_hummingbot(hummingbot_type) + kujira_type_from_name = KujiraOrderType.from_name(kujira_type.name) + + self.assertEqual(item, kujira_type) + self.assertEqual(item, kujira_type_from_name) + else: + with self.assertRaises(ValueError) as context: + KujiraOrderType.to_hummingbot(item) + + self.assertEqual(str(context.exception), 'Unrecognized order type "OrderType.MARKET".') diff --git a/test/hummingbot/core/api_throttler/test_async_throttler.py b/test/hummingbot/core/api_throttler/test_async_throttler.py index 1c217a1cc7..422b9bd1d2 100644 --- a/test/hummingbot/core/api_throttler/test_async_throttler.py +++ b/test/hummingbot/core/api_throttler/test_async_throttler.py @@ -61,19 +61,16 @@ def test_init_without_rate_limits_share_pct(self): self.assertEqual(1, self.throttler._id_to_limit_map[TEST_POOL_ID].limit) self.assertEqual(1, self.throttler._id_to_limit_map[TEST_PATH_URL].limit) - @patch("hummingbot.core.api_throttler.async_throttler_base.AsyncThrottlerBase._client_config_map") - def test_init_with_rate_limits_share_pct(self, config_map_mock): + def test_init_with_rate_limits_share_pct(self): rate_share_pct: Decimal = Decimal("55") - self.client_config_map.rate_limits_share_pct = rate_share_pct - config_map_mock.return_value = self.client_config_map - self.throttler = AsyncThrottler(rate_limits=self.rate_limits) + self.throttler = AsyncThrottler(rate_limits=self.rate_limits, limits_share_percentage=rate_share_pct) rate_limits = self.rate_limits.copy() rate_limits.append(RateLimit(limit_id="ANOTHER_TEST", limit=10, time_interval=5)) expected_limit = math.floor(Decimal("10") * rate_share_pct / Decimal("100")) - throttler = AsyncThrottler(rate_limits=rate_limits) + throttler = AsyncThrottler(rate_limits=rate_limits, limits_share_percentage=rate_share_pct) self.assertEqual(0.1, throttler._retry_interval) self.assertEqual(6, len(throttler._rate_limits)) self.assertEqual(Decimal("1"), throttler._id_to_limit_map[TEST_POOL_ID].limit) diff --git a/test/hummingbot/data_feed/candles_feed/kucoin_spot_candles/test_kucoin_spot_candles.py b/test/hummingbot/data_feed/candles_feed/kucoin_spot_candles/test_kucoin_spot_candles.py index c7526b667c..5ab8695d7f 100644 --- a/test/hummingbot/data_feed/candles_feed/kucoin_spot_candles/test_kucoin_spot_candles.py +++ b/test/hummingbot/data_feed/candles_feed/kucoin_spot_candles/test_kucoin_spot_candles.py @@ -214,7 +214,7 @@ def test_listen_for_subscriptions_logs_exception_details(self, mock_ws, sleep_mo self.listening_task = self.ev_loop.create_task(self.data_feed.listen_for_subscriptions()) - self.async_run_with_timeout(self.resume_test_event.wait()) + self.async_run_with_timeout(self.resume_test_event.wait(), timeout=1) self.assertTrue( self.is_logged( @@ -273,6 +273,8 @@ def test_process_websocket_messages_duplicated_candle_not_included(self, ws_conn websocket_mock=ws_connect_mock.return_value, message=json.dumps(self.get_candles_ws_data_mock_1())) + self.data_feed._time = MagicMock(return_value=5) + self.listening_task = self.ev_loop.create_task(self.data_feed.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) diff --git a/test/hummingbot/data_feed/candles_feed/test_candles_factory.py b/test/hummingbot/data_feed/candles_feed/test_candles_factory.py index c24f79a493..017dfcf295 100644 --- a/test/hummingbot/data_feed/candles_feed/test_candles_factory.py +++ b/test/hummingbot/data_feed/candles_feed/test_candles_factory.py @@ -2,20 +2,32 @@ from hummingbot.data_feed.candles_feed.binance_perpetual_candles import BinancePerpetualCandles from hummingbot.data_feed.candles_feed.binance_spot_candles import BinanceSpotCandles -from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory class TestCandlesFactory(unittest.TestCase): def test_get_binance_candles_spot(self): - candles = CandlesFactory.get_candle(connector="binance", trading_pair="ETH-USDT") + candles = CandlesFactory.get_candle(CandlesConfig( + connector="binance", + trading_pair="BTC-USDT", + interval="1m" + )) self.assertIsInstance(candles, BinanceSpotCandles) candles.stop() def test_get_binance_candles_perpetuals(self): - candles = CandlesFactory.get_candle(connector="binance_perpetual", trading_pair="ETH-USDT") + candles = CandlesFactory.get_candle(CandlesConfig( + connector="binance_perpetual", + trading_pair="BTC-USDT", + interval="1m" + )) self.assertIsInstance(candles, BinancePerpetualCandles) candles.stop() def test_get_non_existing_candles(self): with self.assertRaises(Exception): - CandlesFactory.get_candle(connector="hbot", trading_pair="ETH-USDT") + CandlesFactory.get_candle(CandlesConfig( + connector="hbot", + trading_pair="BTC-USDT", + interval="1m" + )) diff --git a/test/hummingbot/smart_components/executors/__init__.py b/test/hummingbot/smart_components/executors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/executors/arbitrage_executor/__init__.py b/test/hummingbot/smart_components/executors/arbitrage_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/arbitrage_executor/test_arbitrage_executor.py b/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py similarity index 96% rename from test/hummingbot/smart_components/arbitrage_executor/test_arbitrage_executor.py rename to test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py index c21a0a4d82..92daf4fd26 100644 --- a/test/hummingbot/smart_components/arbitrage_executor/test_arbitrage_executor.py +++ b/test/hummingbot/smart_components/executors/arbitrage_executor/test_arbitrage_executor.py @@ -6,13 +6,13 @@ from hummingbot.connector.connector_base import ConnectorBase from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.event.events import MarketOrderFailureEvent -from hummingbot.smart_components.arbitrage_executor.arbitrage_executor import ArbitrageExecutor -from hummingbot.smart_components.arbitrage_executor.data_types import ( +from hummingbot.smart_components.executors.arbitrage_executor.arbitrage_executor import ArbitrageExecutor +from hummingbot.smart_components.executors.arbitrage_executor.data_types import ( ArbitrageConfig, ArbitrageExecutorStatus, ExchangePair, ) -from hummingbot.smart_components.position_executor.data_types import TrackedOrder +from hummingbot.smart_components.executors.position_executor.data_types import TrackedOrder from hummingbot.strategy.script_strategy_base import ScriptStrategyBase diff --git a/test/hummingbot/smart_components/executors/position_executor/__init__.py b/test/hummingbot/smart_components/executors/position_executor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/position_executor/test_data_types.py b/test/hummingbot/smart_components/executors/position_executor/test_data_types.py similarity index 97% rename from test/hummingbot/smart_components/position_executor/test_data_types.py rename to test/hummingbot/smart_components/executors/position_executor/test_data_types.py index 05a202a458..54bc378008 100644 --- a/test/hummingbot/smart_components/position_executor/test_data_types.py +++ b/test/hummingbot/smart_components/executors/position_executor/test_data_types.py @@ -3,7 +3,7 @@ from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder -from hummingbot.smart_components.position_executor.data_types import ( +from hummingbot.smart_components.executors.position_executor.data_types import ( CloseType, PositionConfig, PositionExecutorStatus, diff --git a/test/hummingbot/smart_components/position_executor/test_position_executor.py b/test/hummingbot/smart_components/executors/position_executor/test_position_executor.py similarity index 96% rename from test/hummingbot/smart_components/position_executor/test_position_executor.py rename to test/hummingbot/smart_components/executors/position_executor/test_position_executor.py index f250245665..9889aee6d9 100644 --- a/test/hummingbot/smart_components/position_executor/test_position_executor.py +++ b/test/hummingbot/smart_components/executors/position_executor/test_position_executor.py @@ -13,13 +13,13 @@ OrderFilledEvent, ) from hummingbot.logger import HummingbotLogger -from hummingbot.smart_components.position_executor.data_types import ( +from hummingbot.smart_components.executors.position_executor.data_types import ( CloseType, PositionConfig, PositionExecutorStatus, TrailingStop, ) -from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor from hummingbot.strategy.script_strategy_base import ScriptStrategyBase @@ -149,7 +149,7 @@ async def test_control_position_order_placed_not_cancel_open_order(self): position_executor._strategy.cancel.assert_not_called() position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) async def test_control_position_active_position_create_take_profit(self, _): position_config = self.get_position_config_market_short() type(self.strategy).current_timestamp = PropertyMock(return_value=1234567890) @@ -185,7 +185,7 @@ async def test_control_position_active_position_create_take_profit(self, _): self.assertEqual(position_executor.trade_pnl, Decimal("-0.01")) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("120")) async def test_control_position_active_position_close_by_take_profit_market(self, _): position_config = self.get_position_config_market_long_tp_market() @@ -224,7 +224,7 @@ async def test_control_position_active_position_close_by_take_profit_market(self self.assertEqual(position_executor.trade_pnl, Decimal("0.2")) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("70")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("70")) async def test_control_position_active_position_close_by_stop_loss(self, _): position_config = self.get_position_config_market_long() type(self.strategy).current_timestamp = PropertyMock(return_value=1234567890) @@ -262,7 +262,7 @@ async def test_control_position_active_position_close_by_stop_loss(self, _): self.assertEqual(position_executor.trade_pnl, Decimal("-0.3")) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("100")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("100")) async def test_control_position_active_position_close_by_time_limit(self, _): position_config = self.get_position_config_market_long() type(self.strategy).current_timestamp = PropertyMock(return_value=1234597890) @@ -300,7 +300,7 @@ async def test_control_position_active_position_close_by_time_limit(self, _): self.assertEqual(position_executor.trade_pnl, Decimal("0.0")) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("70")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("70")) async def test_control_position_close_placed_stop_loss_failed(self, _): position_config = self.get_position_config_market_long() type(self.strategy).current_timestamp = PropertyMock(return_value=1234567890) @@ -446,7 +446,7 @@ def test_process_order_filled_event_open_order_started(self): self.assertEqual(position_executor.executor_status, PositionExecutorStatus.ACTIVE_POSITION) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) def test_to_format_status(self, _): position_config = self.get_position_config_market_long() type(self.strategy).current_timestamp = PropertyMock(return_value=1234567890) @@ -482,7 +482,7 @@ def test_to_format_status(self, _): self.assertIn("PNL (%): 0.80%", status[0]) position_executor.terminate_control_loop() - @patch("hummingbot.smart_components.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) + @patch("hummingbot.smart_components.executors.position_executor.position_executor.PositionExecutor.get_price", return_value=Decimal("101")) def test_to_format_status_is_closed(self, _): position_config = self.get_position_config_market_long() type(self.strategy).current_timestamp = PropertyMock(return_value=1234567890) diff --git a/test/hummingbot/smart_components/strategy_frameworks/__init__.py b/test/hummingbot/smart_components/strategy_frameworks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/strategy_frameworks/directional_trading/__init__.py b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_backtesting_engine.py b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_backtesting_engine.py new file mode 100644 index 0000000000..7f233a4f3b --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_backtesting_engine.py @@ -0,0 +1,95 @@ +import unittest +from datetime import datetime, timezone +from decimal import Decimal +from unittest.mock import Mock + +import pandas as pd + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf +from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_backtesting_engine import ( + DirectionalTradingBacktestingEngine, +) + + +class TestDirectionalTradingBacktestingEngine(unittest.TestCase): + def get_controller_mock_simple(self): + controller_base_mock = Mock() + controller_base_mock.config.order_levels = [ + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"), + triple_barrier_conf=TripleBarrierConf(take_profit=Decimal("0.2"), + stop_loss=Decimal("0.1"), + time_limit=360)), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("10"), + triple_barrier_conf=TripleBarrierConf(take_profit=Decimal("0.2"), + stop_loss=Decimal("0.1"), + time_limit=360)) + ] + initial_date = datetime(2023, 3, 16, 0, 0, tzinfo=timezone.utc) + initial_timestamp = int(initial_date.timestamp()) + minute = 60 + timestamps = [ + initial_timestamp, + initial_timestamp + minute * 2, + initial_timestamp + minute * 4, + initial_timestamp + minute * 6, + initial_timestamp + minute * 8 + ] + + controller_base_mock.get_processed_data = Mock(return_value=pd.DataFrame({ + "timestamp": timestamps, + "close": [100, 110, 110, 130, 100], + "signal": [1, 1, -1, -1, 1] + })) + return controller_base_mock + + def get_controller_mock_with_cooldown(self): + controller_base_mock = Mock() + controller_base_mock.config.order_levels = [ + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"), + cooldown_time=60, + triple_barrier_conf=TripleBarrierConf(take_profit=Decimal("0.2"), + stop_loss=Decimal("0.1"), + time_limit=360) + ), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("10"), + cooldown_time=60, + triple_barrier_conf=TripleBarrierConf(take_profit=Decimal("0.2"), + stop_loss=Decimal("0.1"), + time_limit=360)) + ] + initial_date = datetime(2023, 3, 16, 0, 0, tzinfo=timezone.utc) + initial_timestamp = int(initial_date.timestamp()) + minute = 60 + timestamps = [ + initial_timestamp, + initial_timestamp + minute * 2, + initial_timestamp + minute * 4, + initial_timestamp + minute * 6, + initial_timestamp + minute * 8 + ] + + controller_base_mock.get_processed_data = Mock(return_value=pd.DataFrame({ + "timestamp": timestamps, + "close": [100, 110, 110, 130, 100], + "signal": [1, 1, -1, -1, 1] + })) + return controller_base_mock + + def test_run_backtesting_all_positions(self): + engine = DirectionalTradingBacktestingEngine(self.get_controller_mock_simple()) + backtesting_results = engine.run_backtesting() + self.assertIsInstance(backtesting_results, dict) + processed_data = backtesting_results["processed_data"] + self.assertIn("signal", processed_data.columns) + + executors_df = backtesting_results["executors_df"] + self.assertIn("side", executors_df.columns) + self.assertEqual(4, len(executors_df)) + self.assertEqual(2, len(executors_df[executors_df["profitable"] == 1])) + + def test_run_backtesting_with_cooldown(self): + engine = DirectionalTradingBacktestingEngine(self.get_controller_mock_with_cooldown()) + backtesting_results = engine.run_backtesting() + executors_df = backtesting_results["executors_df"] + self.assertEqual(2, len(executors_df)) diff --git a/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_controller_base.py b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_controller_base.py new file mode 100644 index 0000000000..91e7494907 --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_controller_base.py @@ -0,0 +1,124 @@ +import unittest +from decimal import Decimal +from unittest.mock import MagicMock, patch + +import pandas as pd + +from hummingbot.core.data_type.common import TradeType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf +from hummingbot.smart_components.strategy_frameworks.directional_trading import ( + DirectionalTradingControllerBase, + DirectionalTradingControllerConfigBase, +) + + +class TestDirectionalTradingControllerBase(unittest.TestCase): + + def setUp(self): + self.mock_candles_config = CandlesConfig( + connector="binance", + trading_pair="BTC-USDT", + interval="1m" + ) + # Mocking the DirectionalTradingControllerConfigBase + self.mock_controller_config = DirectionalTradingControllerConfigBase( + strategy_name="directional_strategy", + exchange="binance", + trading_pair="BTC-USDT", + candles_config=[self.mock_candles_config], + order_levels=[], + ) + + # Instantiating the DirectionalTradingControllerBase + self.controller = DirectionalTradingControllerBase( + config=self.mock_controller_config, + ) + + def test_filter_executors_df(self): + mock_df = pd.DataFrame({"trading_pair": ["BTC-USDT", "ETH-USDT"]}) + self.controller.filter_executors_df = MagicMock(return_value=mock_df[mock_df["trading_pair"] == "BTC-USDT"]) + filtered_df = self.controller.filter_executors_df(mock_df) + self.assertEqual(len(filtered_df), 1) + + def test_update_strategy_markets_dict(self): + markets_dict = {} + updated_markets_dict = self.controller.update_strategy_markets_dict(markets_dict) + self.assertEqual(updated_markets_dict, {"binance": {"BTC-USDT"}}) + + def test_is_perpetual(self): + self.controller.config.exchange = "binance_perpetual" + self.assertTrue(self.controller.is_perpetual) + + def test_get_signal(self): + mock_df = pd.DataFrame({"signal": [1, -1, 1]}) + self.controller.get_processed_data = MagicMock(return_value=mock_df) + signal = self.controller.get_signal() + self.assertEqual(signal, 1) + + def test_early_stop_condition(self): + with self.assertRaises(NotImplementedError): + self.controller.early_stop_condition(None, None) + + def test_cooldown_condition(self): + with self.assertRaises(NotImplementedError): + self.controller.cooldown_condition(None, None) + + def test_get_processed_data(self): + with self.assertRaises(NotImplementedError): + self.controller.get_processed_data() + + @patch( + "hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base.format_df_for_printout") + def test_to_format_status(self, mock_format_df_for_printout): + # Create a mock DataFrame + mock_df = pd.DataFrame({ + "timestamp": ["2021-01-01", "2021-01-02", "2021-01-03", "2021-01-04"], + "open": [1, 2, 3, 4], + "low": [1, 2, 3, 4], + "high": [1, 2, 3, 4], + "close": [1, 2, 3, 4], + "volume": [1, 2, 3, 4], + "signal": [1, -1, 1, -1] + }) + + # Mock the get_processed_data method to return the mock DataFrame + self.controller.get_processed_data = MagicMock(return_value=mock_df) + + # Mock the format_df_for_printout function to return a sample formatted string + mock_format_df_for_printout.return_value = "formatted_string" + + # Call the method and get the result + result = self.controller.to_format_status() + + # Check if the result contains the expected formatted string + self.assertIn("formatted_string", result) + + @patch("hummingbot.smart_components.strategy_frameworks.controller_base.ControllerBase.get_close_price") + def test_get_position_config(self, mock_get_closest_price): + order_level = OrderLevel( + level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"), + triple_barrier_conf=TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 2, + )) + mock_get_closest_price.return_value = Decimal("100") + # Create a mock DataFrame + mock_df = pd.DataFrame({ + "timestamp": ["2021-01-01", "2021-01-02", "2021-01-03", "2021-01-04"], + "open": [1, 2, 3, 4], + "low": [1, 2, 3, 4], + "high": [1, 2, 3, 4], + "close": [1, 2, 3, 4], + "volume": [1, 2, 3, 4], + "signal": [1, -1, 1, -1] + }) + + # Mock the get_processed_data method to return the mock DataFrame + self.controller.get_processed_data = MagicMock(return_value=mock_df) + position_config = self.controller.get_position_config(order_level, 1) + self.assertEqual(position_config.trading_pair, "BTC-USDT") + self.assertEqual(position_config.exchange, "binance") + self.assertEqual(position_config.side, TradeType.BUY) + self.assertEqual(position_config.amount, Decimal("0.1")) + self.assertEqual(position_config.entry_price, Decimal("100")) diff --git a/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_executor_handler.py b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_executor_handler.py new file mode 100644 index 0000000000..fb4a70ea5e --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/directional_trading/test_directional_trading_executor_handler.py @@ -0,0 +1,100 @@ +from decimal import Decimal +from test.isolated_asyncio_wrapper_test_case import IsolatedAsyncioWrapperTestCase +from unittest.mock import MagicMock, patch + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorStatus +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf +from hummingbot.smart_components.strategy_frameworks.directional_trading import ( + DirectionalTradingControllerBase, + DirectionalTradingControllerConfigBase, + DirectionalTradingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class TestDirectionalTradingExecutorHandler(IsolatedAsyncioWrapperTestCase): + + def setUp(self): + # Mocking the necessary components + self.mock_strategy = MagicMock(spec=ScriptStrategyBase) + self.mock_controller = MagicMock(spec=DirectionalTradingControllerBase) + triple_barrier_conf = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 24, + trailing_stop_activation_price_delta=Decimal("0.002"), + trailing_stop_trailing_delta=Decimal("0.0005") + ) + self.mock_controller.config = MagicMock(spec=DirectionalTradingControllerConfigBase) + self.mock_controller.config.exchange = "binance" + self.mock_controller.config.trading_pair = "BTC-USDT" + self.mock_controller.config.order_levels = [ + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("100"), + spread_factor=Decimal("0.01"), triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("100"), + spread_factor=Decimal("0.01"), triple_barrier_conf=triple_barrier_conf) + ] + + # Instantiating the DirectionalTradingExecutorHandler + self.handler = DirectionalTradingExecutorHandler( + strategy=self.mock_strategy, + controller=self.mock_controller + ) + + @patch( + "hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.close_open_positions") + def test_on_stop_perpetual(self, mock_close_open_positions): + self.mock_controller.is_perpetual = True + self.handler.on_stop() + mock_close_open_positions.assert_called_once() + + @patch( + "hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.close_open_positions") + def test_on_stop_non_perpetual(self, mock_close_open_positions): + self.mock_controller.is_perpetual = False + self.handler.on_stop() + mock_close_open_positions.assert_not_called() + + @patch( + "hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_executor_handler.DirectionalTradingExecutorHandler.set_leverage_and_position_mode") + def test_on_start_perpetual(self, mock_set_leverage): + self.mock_controller.is_perpetual = True + self.handler.on_start() + mock_set_leverage.assert_called_once() + + @patch( + "hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_executor_handler.DirectionalTradingExecutorHandler.set_leverage_and_position_mode") + def test_on_start_non_perpetual(self, mock_set_leverage): + self.mock_controller.is_perpetual = False + self.handler.on_start() + mock_set_leverage.assert_not_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_all_candles_ready(self, mock_create_executor): + self.mock_controller.all_candles_ready = True + await self.handler.control_task() + mock_create_executor.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_candles_not_ready(self, mock_create_executor): + self.mock_controller.all_candles_ready = False + await self.handler.control_task() + mock_create_executor.assert_not_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.store_executor") + async def test_control_task_executor_closed_not_in_cooldown(self, mock_store_executor): + self.mock_controller.all_candles_ready = True + mock_executor = MagicMock() + mock_executor.is_closed = True + mock_executor.executor_status = PositionExecutorStatus.COMPLETED + self.handler.level_executors["BUY_1"] = mock_executor + self.handler.level_executors["SELL_1"] = mock_executor + self.mock_controller.cooldown_condition.return_value = False + await self.handler.control_task() + mock_store_executor.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_no_executor(self, mock_create_executor): + self.mock_controller.all_candles_ready = True + await self.handler.control_task() + mock_create_executor.assert_called() diff --git a/test/hummingbot/smart_components/strategy_frameworks/market_making/__init__.py b/test/hummingbot/smart_components/strategy_frameworks/market_making/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_controller_base.py b/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_controller_base.py new file mode 100644 index 0000000000..550a78de0e --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_controller_base.py @@ -0,0 +1,75 @@ +import unittest +from unittest.mock import MagicMock + +import pandas as pd + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.strategy_frameworks.market_making import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, +) + + +class TestMarketMakingControllerBase(unittest.TestCase): + + def setUp(self): + # Mocking the CandlesConfig + self.mock_candles_config = CandlesConfig( + connector="binance", + trading_pair="BTC-USDT", + interval="1m" + ) + + # Mocking the MarketMakingControllerConfigBase + self.mock_controller_config = MarketMakingControllerConfigBase( + strategy_name="dman_strategy", + exchange="binance", + trading_pair="BTC-USDT", + candles_config=[self.mock_candles_config], + order_levels=[] + ) + + # Instantiating the MarketMakingControllerBase + self.controller = MarketMakingControllerBase( + config=self.mock_controller_config, + ) + + def test_get_price_and_spread_multiplier(self): + mock_candles_df = pd.DataFrame({"price_multiplier": [1.0, 2.0, 3.0], "spread_multiplier": [0.1, 0.2, 0.3]}) + self.controller.get_processed_data = MagicMock(return_value=mock_candles_df) + price_multiplier, spread_multiplier = self.controller.get_price_and_spread_multiplier() + self.assertEqual(price_multiplier, 3.0) + self.assertEqual(spread_multiplier, 0.3) + + def test_update_strategy_markets_dict(self): + markets_dict = {} + updated_markets_dict = self.controller.update_strategy_markets_dict(markets_dict) + self.assertEqual(updated_markets_dict, {"binance": {"BTC-USDT"}}) + + def test_is_perpetual_true(self): + self.controller.config.exchange = "mock_exchange_perpetual" + self.assertTrue(self.controller.is_perpetual) + + def test_is_perpetual_false(self): + self.controller.config.exchange = "mock_regular_exchange" + self.assertFalse(self.controller.is_perpetual) + + def test_refresh_order_condition(self): + with self.assertRaises(NotImplementedError): + self.controller.refresh_order_condition(None, None) + + def test_early_stop_condition(self): + with self.assertRaises(NotImplementedError): + self.controller.early_stop_condition(None, None) + + def test_cooldown_condition(self): + with self.assertRaises(NotImplementedError): + self.controller.cooldown_condition(None, None) + + def test_get_position_config(self): + with self.assertRaises(NotImplementedError): + self.controller.get_position_config(None) + + def test_get_candles_with_price_and_spread_multipliers(self): + with self.assertRaises(NotImplementedError): + self.controller.get_processed_data() diff --git a/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_executor_handler.py b/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_executor_handler.py new file mode 100644 index 0000000000..2660bfb2fa --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/market_making/test_market_making_executor_handler.py @@ -0,0 +1,110 @@ +from decimal import Decimal +from test.isolated_asyncio_wrapper_test_case import IsolatedAsyncioWrapperTestCase +from unittest.mock import MagicMock, patch + +from hummingbot.core.data_type.common import TradeType +from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorStatus +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf +from hummingbot.smart_components.strategy_frameworks.market_making import ( + MarketMakingControllerBase, + MarketMakingControllerConfigBase, + MarketMakingExecutorHandler, +) +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class TestMarketMakingExecutorHandler(IsolatedAsyncioWrapperTestCase): + + def setUp(self): + # Mocking the necessary components + self.mock_strategy = MagicMock(spec=ScriptStrategyBase) + self.mock_controller = MagicMock(spec=MarketMakingControllerBase) + triple_barrier_conf = TripleBarrierConf( + stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"), + time_limit=60 * 60 * 24, + trailing_stop_activation_price_delta=Decimal("0.002"), + trailing_stop_trailing_delta=Decimal("0.0005") + ) + self.mock_controller.config = MagicMock(spec=MarketMakingControllerConfigBase) + self.mock_controller.config.exchange = "binance" + self.mock_controller.config.trading_pair = "BTC-USDT" + self.mock_controller.config.order_levels = [ + OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("100"), + spread_factor=Decimal("0.01"), triple_barrier_conf=triple_barrier_conf), + OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("100"), + spread_factor=Decimal("0.01"), triple_barrier_conf=triple_barrier_conf) + ] + + # Instantiating the MarketMakingExecutorHandler + self.handler = MarketMakingExecutorHandler( + strategy=self.mock_strategy, + controller=self.mock_controller + ) + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.close_open_positions") + def test_on_stop_perpetual(self, mock_close_open_positions): + self.mock_controller.is_perpetual = True + self.handler.on_stop() + mock_close_open_positions.assert_called_once() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.close_open_positions") + def test_on_stop_non_perpetual(self, mock_close_open_positions): + self.mock_controller.is_perpetual = False + self.handler.on_stop() + mock_close_open_positions.assert_not_called() + + @patch("hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler.MarketMakingExecutorHandler.set_leverage_and_position_mode") + def test_on_start_perpetual(self, mock_set_leverage): + self.mock_controller.is_perpetual = True + self.handler.on_start() + mock_set_leverage.assert_called_once() + + @patch("hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler.MarketMakingExecutorHandler.set_leverage_and_position_mode") + def test_on_start_non_perpetual(self, mock_set_leverage): + self.mock_controller.is_perpetual = False + self.handler.on_start() + mock_set_leverage.assert_not_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_all_candles_ready(self, mock_create_executor): + self.mock_controller.all_candles_ready = True + await self.handler.control_task() + mock_create_executor.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_candles_not_ready(self, mock_create_executor): + self.mock_controller.all_candles_ready = False + await self.handler.control_task() + mock_create_executor.assert_not_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.store_executor") + async def test_control_task_executor_closed_not_in_cooldown(self, mock_store_executor): + self.mock_controller.all_candles_ready = True + mock_executor = MagicMock() + mock_executor.is_closed = True + mock_executor.executor_status = PositionExecutorStatus.COMPLETED + self.handler.level_executors["BUY_1"] = mock_executor + self.handler.level_executors["SELL_1"] = mock_executor + self.mock_controller.cooldown_condition.return_value = False + + await self.handler.control_task() + mock_store_executor.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.store_executor") + async def test_control_task_executor_not_started_refresh_order(self, _): + self.mock_controller.all_candles_ready = True + mock_executor = MagicMock() + mock_executor.is_closed = False + mock_executor.executor_status = PositionExecutorStatus.NOT_STARTED + self.handler.level_executors["BUY_1"] = mock_executor + self.handler.level_executors["SELL_1"] = mock_executor + self.mock_controller.refresh_order_condition.return_value = True + + await self.handler.control_task() + mock_executor.early_stop.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.ExecutorHandlerBase.create_executor") + async def test_control_task_no_executor(self, mock_create_executor): + self.mock_controller.all_candles_ready = True + await self.handler.control_task() + mock_create_executor.assert_called() diff --git a/test/hummingbot/smart_components/strategy_frameworks/test_backtesting_engine_base.py b/test/hummingbot/smart_components/strategy_frameworks/test_backtesting_engine_base.py new file mode 100644 index 0000000000..cd2b1dee40 --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/test_backtesting_engine_base.py @@ -0,0 +1,94 @@ +import unittest +from datetime import datetime, timezone +from unittest.mock import patch + +import pandas as pd + +from hummingbot.smart_components.strategy_frameworks.backtesting_engine_base import BacktestingEngineBase + + +class TestBacktestingEngineBase(unittest.TestCase): + + @patch("hummingbot.smart_components.strategy_frameworks.controller_base.ControllerBase") + def setUp(self, MockControllerBase): + self.controller = MockControllerBase() + self.backtesting_engine = BacktestingEngineBase(self.controller) + + def test_filter_df_by_time(self): + df = pd.DataFrame({ + "timestamp": pd.date_range(start="2021-01-01", end="2021-01-05", freq="D") + }) + filtered_df = self.backtesting_engine.filter_df_by_time(df, "2021-01-02", "2021-01-04") + self.assertEqual(len(filtered_df), 3) + self.assertEqual(filtered_df["timestamp"].min(), pd.Timestamp("2021-01-02")) + self.assertEqual(filtered_df["timestamp"].max(), pd.Timestamp("2021-01-04")) + + @patch("pandas.read_csv") + def test_get_data(self, mock_read_csv): + mock_df = pd.DataFrame({ + "timestamp": pd.date_range(start="2021-01-01", end="2021-01-05", freq="D") + }) + mock_read_csv.return_value = mock_df + self.controller.get_processed_data.return_value = mock_df + + df = self.backtesting_engine.get_data("2021-01-02", "2021-01-04") + self.assertEqual(len(df), 3) + self.assertEqual(df["timestamp"].min(), pd.Timestamp("2021-01-02")) + self.assertEqual(df["timestamp"].max(), pd.Timestamp("2021-01-04")) + + def test_summarize_results(self): + initial_date = datetime(2023, 3, 16, 0, 0, tzinfo=timezone.utc) + initial_timestamp = int(initial_date.timestamp()) + minute = 60 + timestamps = [ + initial_timestamp, + initial_timestamp + minute * 2, + initial_timestamp + minute * 4, + initial_timestamp + minute * 6, + initial_timestamp + minute * 8 + ] + + # Assuming each trade closes after 60 seconds (1 minute) + close_timestamps = [timestamp + 60 for timestamp in timestamps] + executors_df = pd.DataFrame({ + "timestamp": timestamps, + "close_timestamp": close_timestamps, + "exchange": ["binance_perpetual"] * 5, + "trading_pair": ["HBOT-USDT"] * 5, + "side": ["BUY", "BUY", "SELL", "SELL", "BUY"], + "amount": [10, 20, 10, 20, 10], + "trade_pnl": [0.2, 0.1, -0.1, -0.2, 0.2], + "trade_pnl_quote": [0, 0, 0, 0, 0], + "cum_fee_quote": [0, 0, 0, 0, 0], + "net_pnl": [1, 2, -1, -2, 1], + "net_pnl_quote": [0.1, 0.2, -0.1, -0.2, 0.1], + "profitable": [1, 1, 0, 0, 1], + "signal": [1, -1, 1, 1, 1], + "executor_status": ["COMPLETED"] * 5, + "close_type": ["EXPIRED", "EXPIRED", "EXPIRED", "EXPIRED", "EXPIRED"], + "entry_price": [5, 5, 5, 5, 5], + "close_price": [5, 5, 5, 5, 5], + "sl": [0.03] * 5, + "tp": [0.02] * 5, + "tl": [86400] * 5, + "leverage": [10] * 5, + "inventory": [10, 10, 10, 10, 10], + }) + executors_df.index = pd.to_datetime(executors_df["timestamp"], unit="s") + executors_df["close_time"] = pd.to_datetime(executors_df["close_timestamp"], unit="s") + result = self.backtesting_engine.summarize_results(executors_df) + self.assertEqual(result["net_pnl"], 1) # 1 + 2 - 1 - 2 + 1 + self.assertEqual(round(result["net_pnl_quote"], 2), 0.1) # 0.1 + 0.2 - 0.1 - 0.2 + 0.1 + self.assertEqual(result["total_executors"], 5) + self.assertEqual(result["total_executors_with_position"], 5) + self.assertEqual(result["total_long"], 3) # 3 BUYs + self.assertEqual(result["total_short"], 2) # 2 SELLs + self.assertEqual(result["close_types"]["EXPIRED"], 5) # All are "EXPIRED" + self.assertEqual(result["accuracy"], 3 / 5) # 3 out of 5 trades were profitable + self.assertEqual(round(result["duration_minutes"], 3), 9) # 4 minutes between the first and last trade + self.assertEqual(round(result["avg_trading_time_minutes"], 3), 1) # Average of 1 minute between trades + + def test_summarize_results_empty(self): + result = self.backtesting_engine.summarize_results(pd.DataFrame()) + self.assertEqual(result["net_pnl"], 0) + self.assertEqual(result["net_pnl_quote"], 0) diff --git a/test/hummingbot/smart_components/strategy_frameworks/test_controller_base.py b/test/hummingbot/smart_components/strategy_frameworks/test_controller_base.py new file mode 100644 index 0000000000..6680eea0bc --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/test_controller_base.py @@ -0,0 +1,89 @@ +import unittest +from unittest.mock import MagicMock + +import pandas as pd + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase, ControllerConfigBase + + +class TestControllerBase(unittest.TestCase): + + def setUp(self): + # Mocking the CandlesConfig + self.mock_candles_config = CandlesConfig( + connector="binance", + trading_pair="BTC-USDT", + interval="1m" + ) + + # Mocking the ControllerConfigBase + self.mock_controller_config = ControllerConfigBase( + strategy_name="dman_strategy", + candles_config=[self.mock_candles_config], + order_levels=[] + ) + + # Instantiating the ControllerBase + self.controller = ControllerBase( + config=self.mock_controller_config, + ) + + def test_initialize_candles_live_mode(self): + candles = self.controller.initialize_candles([self.mock_candles_config]) + self.assertTrue(len(candles) == 1) + + def test_initialize_candles_non_live_mode(self): + self.controller.initialize_candles([self.mock_candles_config]) + self.assertTrue(len(self.controller.candles) == 1) + + def test_get_close_price(self): + mock_candle = MagicMock() + mock_candle.name = "binance_BTC-USDT" + mock_candle.interval = "1m" + mock_candle.candles_df = pd.DataFrame({"close": [100.0, 200.0, 300.0], + "open": [100.0, 200.0, 300.0]}) + self.controller.candles = [mock_candle] + close_price = self.controller.get_close_price("binance", "BTC-USDT") + self.assertEqual(close_price, 300) + + def test_get_candles_by_connector_trading_pair(self): + mock_candle = MagicMock() + mock_candle.name = "binance_BTC-USDT" + mock_candle.interval = "1m" + result = self.controller.get_candles_by_connector_trading_pair("binance", "BTC-USDT") + self.assertEqual(list(result.keys()), ["1m"]) + + def test_get_candle(self): + mock_candle = MagicMock() + mock_candle.name = "binance_BTC-USDT" + mock_candle.interval = "1m" + self.controller.candles = [mock_candle] + result = self.controller.get_candle("binance", "BTC-USDT", "1m") + self.assertEqual(result, mock_candle) + + def test_all_candles_ready(self): + mock_candle = MagicMock() + mock_candle.is_ready = True + self.controller.candles = [mock_candle] + self.assertTrue(self.controller.all_candles_ready) + + def test_start(self): + mock_candle = MagicMock() + self.controller.candles = [mock_candle] + self.controller.start() + mock_candle.start.assert_called_once() + + def test_stop(self): + mock_candle = MagicMock() + self.controller.candles = [mock_candle] + self.controller.stop() + mock_candle.stop.assert_called_once() + + def test_get_csv_prefix(self): + prefix = self.controller.get_csv_prefix() + self.assertEqual(prefix, "dman_strategy") + + def test_to_format_status(self): + status = self.controller.to_format_status() + self.assertEqual(" strategy_name: dman_strategy", status[1]) diff --git a/test/hummingbot/smart_components/strategy_frameworks/test_executor_handler_base.py b/test/hummingbot/smart_components/strategy_frameworks/test_executor_handler_base.py new file mode 100644 index 0000000000..2b4f6ddbab --- /dev/null +++ b/test/hummingbot/smart_components/strategy_frameworks/test_executor_handler_base.py @@ -0,0 +1,134 @@ +import random +from test.isolated_asyncio_wrapper_test_case import IsolatedAsyncioWrapperTestCase +from unittest.mock import AsyncMock, MagicMock, patch + +import pandas as pd + +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide +from hummingbot.smart_components.strategy_frameworks.controller_base import ControllerBase +from hummingbot.smart_components.strategy_frameworks.executor_handler_base import ExecutorHandlerBase + + +class TestExecutorHandlerBase(IsolatedAsyncioWrapperTestCase): + def setUp(self): + super().setUp() + self.mock_strategy = MagicMock() + self.mock_controller = MagicMock(spec=ControllerBase) + self.mock_controller.config = MagicMock() + self.mock_controller.config.order_levels = [] + self.mock_controller.get_csv_prefix = MagicMock(return_value="test_strategy") + self.executor_handler = ExecutorHandlerBase(self.mock_strategy, self.mock_controller) + + def test_initialization(self): + self.assertEqual(self.executor_handler.strategy, self.mock_strategy) + self.assertEqual(self.executor_handler.controller, self.mock_controller) + # ... other assertions ... + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.safe_ensure_future") + def test_start(self, mock_safe_ensure_future): + self.executor_handler.start() + self.mock_controller.start.assert_called_once() + mock_safe_ensure_future.assert_called_once() + + def test_terminate_control_loop(self): + self.executor_handler.stop() + self.assertTrue(self.executor_handler.terminated.is_set()) + + def test_to_format_status(self): + status = self.executor_handler.to_format_status() + self.assertIsInstance(status, str) + + def test_on_stop(self): + self.executor_handler.on_stop() + self.mock_controller.stop.assert_called_once() + + def test_get_csv_path(self): + path = self.executor_handler.get_csv_path() + self.assertEqual(path.suffix, ".csv") + self.assertIn("test_strategy", path.name) + + @patch("pandas.DataFrame.to_csv", new_callable=MagicMock) + def test_store_executor(self, _): + mock_executor = MagicMock() + mock_executor.to_json = MagicMock(return_value={"test": "test"}) + mock_order_level = MagicMock() + self.executor_handler.store_executor(mock_executor, mock_order_level) + self.assertIsNone(self.executor_handler.level_executors[mock_order_level.level_id]) + + @patch.object(ExecutorHandlerBase, "_sleep", new_callable=AsyncMock) + @patch.object(ExecutorHandlerBase, "control_task", new_callable=AsyncMock) + async def test_control_loop(self, mock_control_task, mock_sleep): + mock_sleep.side_effect = [None, Exception] + with self.assertRaises(Exception): + await self.executor_handler.control_loop() + mock_control_task.assert_called() + + @patch("hummingbot.smart_components.strategy_frameworks.executor_handler_base.PositionExecutor") + def test_create_executor(self, mock_position_executor): + mock_position_config = MagicMock() + mock_order_level = MagicMock() + self.executor_handler.create_executor(mock_position_config, mock_order_level) + mock_position_executor.assert_called_once_with(self.mock_strategy, mock_position_config) + self.assertIsNotNone(self.executor_handler.level_executors[mock_order_level.level_id]) + + def generate_random_data(self, num_rows): + data = { + "net_pnl": [random.uniform(-1, 1) for _ in range(num_rows)], + "net_pnl_quote": [random.uniform(0, 1000) for _ in range(num_rows)], + "amount": [random.uniform(0, 100) for _ in range(num_rows)], + "side": [random.choice(["BUY", "SELL"]) for _ in range(num_rows)], + "close_type": [random.choice(["type1", "type2", "type3"]) for _ in range(num_rows)], + "timestamp": [pd.Timestamp.now() for _ in range(num_rows)] + } + return pd.DataFrame(data) + + def test_summarize_executors_df(self): + df = self.generate_random_data(100) # Generate a DataFrame with 100 rows of random data + + summary = ExecutorHandlerBase.summarize_executors_df(df) + + # Check if the summary values match the DataFrame's values + self.assertEqual(summary["net_pnl"], df["net_pnl"].sum()) + self.assertEqual(summary["net_pnl_quote"], df["net_pnl_quote"].sum()) + self.assertEqual(summary["total_executors"], df.shape[0]) + self.assertEqual(summary["total_executors_with_position"], df[df["net_pnl"] != 0].shape[0]) + self.assertEqual(summary["total_volume"], df[df["net_pnl"] != 0]["amount"].sum() * 2) + self.assertEqual(summary["total_long"], (df[df["net_pnl"] != 0]["side"] == "BUY").sum()) + self.assertEqual(summary["total_short"], (df[df["net_pnl"] != 0]["side"] == "SELL").sum()) + + def test_close_open_positions(self): + # Mocking the connector and its methods + mock_connector = MagicMock() + mock_connector.get_mid_price.return_value = 100 # Mocking the mid price to be 100 + + # Mocking the account_positions of the connector + mock_position1 = MagicMock(trading_pair="BTC-USD", position_side=PositionSide.LONG, amount=10) + mock_position2 = MagicMock(trading_pair="BTC-USD", position_side=PositionSide.SHORT, amount=-10) + mock_connector.account_positions = { + "pos1": mock_position1, + "pos2": mock_position2 + } + + # Setting the mock connector to the strategy's connectors + self.mock_strategy.connectors = {"mock_connector": mock_connector} + + # Calling the method + self.executor_handler.close_open_positions(connector_name="mock_connector", trading_pair="BTC-USD") + + # Asserting that the strategy's sell and buy methods were called with the expected arguments + self.mock_strategy.sell.assert_called_once_with( + connector_name="mock_connector", + trading_pair="BTC-USD", + amount=10, + order_type=OrderType.MARKET, + price=100, + position_action=PositionAction.CLOSE + ) + self.mock_strategy.buy.assert_called_once_with( + connector_name="mock_connector", + trading_pair="BTC-USD", + amount=10, + order_type=OrderType.MARKET, + price=100, + position_action=PositionAction.CLOSE + ) diff --git a/test/hummingbot/strategy/test_market_trading_pair_tuple.py b/test/hummingbot/strategy/test_market_trading_pair_tuple.py index 8aa7bea6b1..d1a1ef2696 100644 --- a/test/hummingbot/strategy/test_market_trading_pair_tuple.py +++ b/test/hummingbot/strategy/test_market_trading_pair_tuple.py @@ -275,14 +275,14 @@ def test_get_price_by_type(self): def test_vwap_for_volume(self): # Check VWAP on BUY sell - order_volume: Decimal = Decimal("15") + order_volume = 15 filled_orders: List[OrderBookRow] = self.market.get_order_book(self.trading_pair).simulate_buy(order_volume) expected_vwap: Decimal = sum([Decimal(o.price) * Decimal(o.amount) for o in filled_orders]) / order_volume self.assertAlmostEqual(expected_vwap, self.market_info.get_vwap_for_volume(True, order_volume).result_price, 3) # Check VWAP on SELL side - order_volume: Decimal = Decimal("15") + order_volume = 15 filled_orders: List[OrderBookRow] = self.market.get_order_book(self.trading_pair).simulate_sell(order_volume) expected_vwap: Decimal = sum([Decimal(o.price) * Decimal(o.amount) for o in filled_orders]) / order_volume @@ -290,14 +290,14 @@ def test_vwap_for_volume(self): def test_get_price_for_volume(self): # Check price on BUY sell - order_volume: Decimal = Decimal("15") + order_volume = 15 filled_orders: List[OrderBookRow] = self.market.get_order_book(self.trading_pair).simulate_buy(order_volume) expected_buy_price: Decimal = max([Decimal(o.price) for o in filled_orders]) self.assertAlmostEqual(expected_buy_price, self.market_info.get_price_for_volume(True, order_volume).result_price, 3) # Check price on SELL side - order_volume: Decimal = Decimal("15") + order_volume = 15 filled_orders: List[OrderBookRow] = self.market.get_order_book(self.trading_pair).simulate_sell(order_volume) expected_sell_price: Decimal = min([Decimal(o.price) for o in filled_orders])