From 30bcdf0637e1b65b04ac5d0991a3fdd4374e1c19 Mon Sep 17 00:00:00 2001 From: bczhang Date: Mon, 15 Jan 2024 16:25:50 +0800 Subject: [PATCH 01/34] (refactor) Create&updates kraken connector to the latest standards --- .../kraken_api_order_book_data_source.py | 427 ++++++------- .../exchange/kraken/kraken_constants.py | 7 + .../exchange/kraken/kraken_exchange.py | 587 ++++++++++++++++++ .../exchange/kraken/kraken_order_book.py | 77 +++ .../exchange/kraken/kraken_web_utils.py | 75 +++ .../connector/exchange/kraken_v1/__init__.py | 0 .../kraken_api_order_book_data_source.py | 290 +++++++++ .../kraken_api_user_stream_data_source.py | 126 ++++ .../exchange/kraken_v1/kraken_auth.py | 49 ++ .../exchange/kraken_v1/kraken_constants.py | 100 +++ .../{kraken => kraken_v1}/kraken_exchange.pxd | 0 .../{kraken => kraken_v1}/kraken_exchange.pyx | 0 .../kraken_in_flight_order.pxd | 0 .../kraken_in_flight_order.pyx | 0 .../kraken_order_book.pxd | 0 .../kraken_order_book.pyx | 0 .../kraken_order_book_tracker.py | 0 .../kraken_tracking_nonce.py | 0 .../kraken_user_stream_tracker.py | 0 .../exchange/kraken_v1/kraken_utils.py | 217 +++++++ 20 files changed, 1720 insertions(+), 235 deletions(-) create mode 100644 hummingbot/connector/exchange/kraken/kraken_exchange.py create mode 100644 hummingbot/connector/exchange/kraken/kraken_order_book.py create mode 100644 hummingbot/connector/exchange/kraken/kraken_web_utils.py create mode 100644 hummingbot/connector/exchange/kraken_v1/__init__.py create mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py create mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py create mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_auth.py create mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_constants.py rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_exchange.pxd (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_exchange.pyx (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_in_flight_order.pxd (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_in_flight_order.pyx (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_order_book.pxd (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_order_book.pyx (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_order_book_tracker.py (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_tracking_nonce.py (100%) rename hummingbot/connector/exchange/{kraken => kraken_v1}/kraken_user_stream_tracker.py (100%) create mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_utils.py diff --git a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py index 7c165667fd..9d0346b9b2 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py @@ -1,12 +1,14 @@ import asyncio import logging import time -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional import pandas as pd from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils + from hummingbot.connector.exchange.kraken.kraken_utils import ( build_api_factory, build_rate_limits_by_tier, @@ -24,267 +26,222 @@ from hummingbot.core.web_assistant.ws_assistant import WSAssistant from hummingbot.logger import HummingbotLogger +if TYPE_CHECKING: + from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange + class KrakenAPIOrderBookDataSource(OrderBookTrackerDataSource): MESSAGE_TIMEOUT = 30.0 - PING_TIMEOUT = 10.0 - - _kraobds_logger: Optional[HummingbotLogger] = None - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._kraobds_logger is None: - cls._kraobds_logger = logging.getLogger(__name__) - return cls._kraobds_logger + # PING_TIMEOUT = 10.0 def __init__(self, - throttler: Optional[AsyncThrottler] = None, - trading_pairs: List[str] = None, - api_factory: Optional[WebAssistantsFactory] = None): + trading_pairs: List[str], + connector: 'KrakenExchange', + api_factory: WebAssistantsFactory, + # throttler: Optional[AsyncThrottler] = None + ): super().__init__(trading_pairs) - self._throttler = throttler or self._get_throttler_instance() - self._api_factory = api_factory or build_api_factory(throttler=throttler) + self._connector = connector + # self._throttler = throttler or self._get_throttler_instance() + # self._api_factory = api_factory or build_api_factory(throttler=throttler) + self._api_factory = api_factory self._rest_assistant = None self._ws_assistant = None self._order_book_create_function = lambda: OrderBook() - @classmethod - def _get_throttler_instance(cls) -> AsyncThrottler: - throttler = AsyncThrottler(build_rate_limits_by_tier()) - return throttler + _kraobds_logger: Optional[HummingbotLogger] = None + # @classmethod + # def _get_throttler_instance(cls) -> AsyncThrottler: + # throttler = AsyncThrottler(build_rate_limits_by_tier()) + # return throttler async def _get_rest_assistant(self) -> RESTAssistant: if self._rest_assistant is None: self._rest_assistant = await self._api_factory.get_rest_assistant() return self._rest_assistant - @classmethod - async def get_last_traded_prices( - cls, trading_pairs: List[str], throttler: Optional[AsyncThrottler] = None - ) -> Dict[str, float]: - throttler = throttler or cls._get_throttler_instance() - tasks = [cls._get_last_traded_price(t_pair, throttler) for t_pair in trading_pairs] - results = await safe_gather(*tasks) - return {t_pair: result for t_pair, result in zip(trading_pairs, results)} - - @classmethod - async def _get_last_traded_price(cls, trading_pair: str, throttler: AsyncThrottler) -> float: - url = ( - f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" - f"?pair={convert_to_exchange_trading_pair(trading_pair)}" - ) - - request = RESTRequest( - method=RESTMethod.GET, - url=url - ) - rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - - async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL): - resp = await rest_assistant.call(request) - resp_json = await resp.json() - record = list(resp_json["result"].values())[0] - return float(record["c"][0]) - - @classmethod - async def get_snapshot( - cls, - rest_assistant: RESTAssistant, - trading_pair: str, - limit: int = 1000, - throttler: Optional[AsyncThrottler] = None, - ) -> Dict[str, Any]: - throttler = throttler or cls._get_throttler_instance() - original_trading_pair: str = trading_pair - if limit != 0: - params = { - "count": str(limit), - "pair": convert_to_exchange_trading_pair(trading_pair) - } - else: - params = {"pair": convert_to_exchange_trading_pair(trading_pair)} - async with throttler.execute_task(CONSTANTS.SNAPSHOT_PATH_URL): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" - - request = RESTRequest( - method=RESTMethod.GET, - url=url, - params=params - ) - - response = await rest_assistant.call(request) - - if response.status != 200: - raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " - f"HTTP status is {response.status}.") - response_json = await response.json() - if len(response_json["error"]) > 0: - raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " - f"Error is {response_json['error']}.") - data: Dict[str, Any] = next(iter(response_json["result"].values())) - data = {"trading_pair": trading_pair, **data} - data["latest_update"] = max([*map(lambda x: x[2], data["bids"] + data["asks"])], default=0.) - - return data - - async def get_new_order_book(self, trading_pair: str) -> OrderBook: - rest_assistant = await self._get_rest_assistant() - snapshot: Dict[str, Any] = await self.get_snapshot( - rest_assistant, trading_pair, limit=1000, throttler=self._throttler - ) + # todo + async def get_last_traded_prices(self, + trading_pairs: List[str], + domain: Optional[str] = None) -> Dict[str, float]: + return await self._connector.get_last_traded_prices(trading_pairs=trading_pairs) + + # + # @classmethod + # async def get_last_traded_prices( + # cls, trading_pairs: List[str], throttler: Optional[AsyncThrottler] = None + # ) -> Dict[str, float]: + # throttler = throttler or cls._get_throttler_instance() + # tasks = [cls._get_last_traded_price(t_pair, throttler) for t_pair in trading_pairs] + # results = await safe_gather(*tasks) + # return {t_pair: result for t_pair, result in zip(trading_pairs, results)} + # + # @classmethod + # async def _get_last_traded_price(cls, trading_pair: str, throttler: AsyncThrottler) -> float: + # url = ( + # f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" + # f"?pair={convert_to_exchange_trading_pair(trading_pair)}" + # ) + # + # request = RESTRequest( + # method=RESTMethod.GET, + # url=url + # ) + # rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() + # + # async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL): + # resp = await rest_assistant.call(request) + # resp_json = await resp.json() + # record = list(resp_json["result"].values())[0] + # return float(record["c"][0]) + + async def _order_book_snapshot(self, trading_pair: str) -> OrderBook: + snapshot: Dict[str, Any] = await self._request_order_book_snapshot(trading_pair) snapshot_timestamp: float = time.time() snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( snapshot, snapshot_timestamp, metadata={"trading_pair": trading_pair} ) - order_book: OrderBook = self.order_book_create_function() - order_book.apply_snapshot(snapshot_msg.bids, snapshot_msg.asks, snapshot_msg.update_id) - return order_book + return snapshot_msg - @classmethod - async def fetch_trading_pairs(cls, throttler: Optional[AsyncThrottler] = None) -> List[str]: - throttler = throttler or cls._get_throttler_instance() - try: - async with throttler.execute_task(CONSTANTS.ASSET_PAIRS_PATH_URL): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - request = RESTRequest( - method=RESTMethod.GET, - url=url - ) - rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - response = await rest_assistant.call(request, timeout=5) - - if response.status == 200: - data: Dict[str, Any] = await response.json() - raw_pairs = data.get("result", []) - converted_pairs: List[str] = [] - for pair, details in raw_pairs.items(): - if "." not in pair: - try: - wsname = details["wsname"] # pair in format BASE/QUOTE - converted_pairs.append(convert_from_exchange_trading_pair(wsname)) - except IOError: - pass - return [item for item in converted_pairs] - except Exception: - pass - # Do nothing if the request fails -- there will be no autocomplete for kraken trading pairs - return [] - - async def listen_for_trades(self, ev_loop: asyncio.AbstractEventLoop, output: asyncio.Queue): - while True: - try: - ws_message: str = await self.get_ws_subscription_message("trade") - - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) - - await ws.send(ws_message) - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - trades = [ - {"pair": convert_from_exchange_trading_pair(msg[-1]), "trade": trade} - for trade in msg[1] - ] - for trade in trades: - trade_msg: OrderBookMessage = KrakenOrderBook.trade_message_from_exchange(trade) - output.put_nowait(trade_msg) - ws.disconnect() - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", - exc_info=True) - await asyncio.sleep(30.0) + async def _request_order_book_snapshot(self, trading_pair: str, ) -> Dict[str, Any]: + """ + Retrieves a copy of the full order book from the exchange, for a particular trading pair. - async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): - while True: - try: - ws_message: str = await self.get_ws_subscription_message("book") - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) + :param trading_pair: the trading pair for which the order book will be retrieved - await ws.send(ws_message) - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - msg_dict = {"trading_pair": convert_from_exchange_trading_pair(msg[-1]), - "asks": msg[1].get("a", []) or msg[1].get("as", []) or [], - "bids": msg[1].get("b", []) or msg[1].get("bs", []) or []} - msg_dict["update_id"] = max( - [*map(lambda x: float(x[2]), msg_dict["bids"] + msg_dict["asks"])], default=0. - ) - if "as" in msg[1] and "bs" in msg[1]: - order_book_message: OrderBookMessage = ( - KrakenOrderBook.snapshot_ws_message_from_exchange(msg_dict, time.time()) - ) - else: - order_book_message: OrderBookMessage = KrakenOrderBook.diff_message_from_exchange( - msg_dict, time.time()) - output.put_nowait(order_book_message) - ws.disconnect() - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", - exc_info=True) - await asyncio.sleep(30.0) + :return: the response from the exchange (JSON dictionary) + """ + params = { + "pair": convert_to_exchange_trading_pair(trading_pair) + } + + rest_assistant = await self._api_factory.get_rest_assistant() + response_json = await rest_assistant.execute_request( + url=web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL), + params=params, + method=RESTMethod.GET, + throttler_limit_id=CONSTANTS.SNAPSHOT_PATH_URL, + ) + if len(response_json["error"]) > 0: + raise IOError(f"Error fetching Kraken market snapshot for {trading_pair}. " + f"Error is {response_json['error']}.") + data: Dict[str, Any] = next(iter(response_json["result"].values())) + data = {"trading_pair": trading_pair, **data} + data["latest_update"] = max([*map(lambda x: x[2], data["bids"] + data["asks"])], default=0.) + return data + + # @classmethod + # async def fetch_trading_pairs(cls, throttler: Optional[AsyncThrottler] = None) -> List[str]: + # throttler = throttler or cls._get_throttler_instance() + # try: + # async with throttler.execute_task(CONSTANTS.ASSET_PAIRS_PATH_URL): + # url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + # request = RESTRequest( + # method=RESTMethod.GET, + # url=url + # ) + # rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() + # response = await rest_assistant.call(request, timeout=5) + # + # if response.status == 200: + # data: Dict[str, Any] = await response.json() + # raw_pairs = data.get("result", []) + # converted_pairs: List[str] = [] + # for pair, details in raw_pairs.items(): + # if "." not in pair: + # try: + # wsname = details["wsname"] # pair in format BASE/QUOTE + # converted_pairs.append(convert_from_exchange_trading_pair(wsname)) + # except IOError: + # pass + # return [item for item in converted_pairs] + # except Exception: + # pass + # # Do nothing if the request fails -- there will be no autocomplete for kraken trading pairs + # return [] + + async def _subscribe_channels(self, ws: WSAssistant): + """ + Subscribes to the trade events and diff orders events through the provided websocket connection. - async def listen_for_order_book_snapshots(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): - rest_assistant = await self._get_rest_assistant() - while True: - try: - for trading_pair in self._trading_pairs: - try: - snapshot: Dict[str, Any] = await self.get_snapshot( - rest_assistant, trading_pair, throttler=self._throttler - ) - snapshot_timestamp: float = time.time() - snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( - snapshot, - snapshot_timestamp, - metadata={"trading_pair": trading_pair} - ) - output.put_nowait(snapshot_msg) - self.logger().debug(f"Saved order book snapshot for {trading_pair}") - await asyncio.sleep(5.0) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error. ", exc_info=True) - await asyncio.sleep(5.0) - this_hour: pd.Timestamp = pd.Timestamp.utcnow().replace(minute=0, second=0, microsecond=0) - next_hour: pd.Timestamp = this_hour + pd.Timedelta(hours=1) - delta: float = next_hour.timestamp() - time.time() - await asyncio.sleep(delta) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error. ", exc_info=True) - await asyncio.sleep(5.0) + :param ws: the websocket assistant used to connect to the exchange + """ + try: + trading_pairs: List[str] = [] + for tp in self._trading_pairs: + # trading_pairs.append(convert_to_exchange_trading_pair(tp, '/')) + symbol = await self._connector.exchange_symbol_associated_to_pair(trading_pair=tp) + trading_pairs.append(symbol) + trades_payload = { + "event": "subscribe", + "pair": trading_pairs, + "subscription": {"name": 'trade', "depth": 1000}, + } + subscribe_trade_request: WSJSONRequest = WSJSONRequest(payload=trades_payload) - async def get_ws_subscription_message(self, subscription_type: str): - trading_pairs: List[str] = [] - for tp in self._trading_pairs: - trading_pairs.append(convert_to_exchange_trading_pair(tp, '/')) + order_book_payload = { + "event": "subscribe", + "pair": trading_pairs, + "subscription": {"name": 'book', "depth": 1000}, + } + subscribe_orderbook_request: WSJSONRequest = WSJSONRequest(payload=order_book_payload) - ws_message: WSJSONRequest = WSJSONRequest({ - "event": "subscribe", - "pair": trading_pairs, - "subscription": {"name": subscription_type, "depth": 1000}}) + await ws.send(subscribe_trade_request) + await ws.send(subscribe_orderbook_request) - return ws_message + self.logger().info("Subscribed to public order book and trade channels...") + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error occurred subscribing to order book data streams.") + raise + + def _channel_originating_message(self, event_message) -> str: + channel = "" + if type(event_message) is list: + channel = self._trade_messages_queue_key if event_message[-2] == CONSTANTS.TRADE_EVENT_TYPE \ + else self._diff_messages_queue_key + else: + if event_message.get("errorMessage") is not None: + err_msg = event_message.get("errorMessage") + raise IOError(f"Error event received from the server ({err_msg})") + return channel + + async def _connected_websocket_assistant(self) -> WSAssistant: + ws: WSAssistant = await self._api_factory.get_ws_assistant() + await ws.connect(ws_url=CONSTANTS.WS_URL, + ping_timeout=CONSTANTS.PING_TIMEOUT) + return ws + + # todo 把convert_from_exchange_trading_pair改掉 + async def _parse_trade_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue): + + trades = [ + # {"pair": convert_from_exchange_trading_pair(raw_message[-1]), "trade": trade} + {"pair": await self._connector.exchange_symbol_associated_to_pair(raw_message[-1]), "trade": trade} + for trade in raw_message[1] + ] + for trade in trades: + trade_msg: OrderBookMessage = KrakenOrderBook.trade_message_from_exchange(trade) + message_queue.put_nowait(trade_msg) + + + async def _parse_order_book_diff_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue): + # msg_dict = {"trading_pair": convert_from_exchange_trading_pair(raw_message[-1]), + msg_dict = {"trading_pair": await self._connector.exchange_symbol_associated_to_pair(raw_message[-1]), + "asks": raw_message[1].get("a", []) or raw_message[1].get("as", []) or [], + "bids": raw_message[1].get("b", []) or raw_message[1].get("bs", []) or []} + msg_dict["update_id"] = max( + [*map(lambda x: float(x[2]), msg_dict["bids"] + msg_dict["asks"])], default=0. + ) + if "as" in raw_message[1] and "bs" in raw_message[1]: + order_book_message: OrderBookMessage = ( + KrakenOrderBook.snapshot_ws_message_from_exchange(msg_dict, time.time()) + ) + else: + order_book_message: OrderBookMessage = KrakenOrderBook.diff_message_from_exchange( + msg_dict, time.time()) + message_queue.put_nowait(order_book_message) - async def listen_for_subscriptions(self): - """ - Connects to the trade events and order diffs websocket endpoints and listens to the messages sent by the - exchange. Each message is stored in its own queue. - """ - # This connector does not use this base class method and needs a refactoring - pass diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index f30dca5323..e170d87774 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -5,6 +5,9 @@ ) from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair +DEFAULT_DOMAIN="kraken" +MAX_ORDER_ID_LEN = 32 +HBOT_ORDER_ID_PREFIX = "HBOT" class KrakenAPITier(Enum): """ @@ -52,6 +55,10 @@ class KrakenAPITier(Enum): WS_URL = "wss://ws.kraken.com" WS_AUTH_URL = "wss://ws-auth.kraken.com/" +DIFF_EVENT_TYPE = "book" +TRADE_EVENT_TYPE = "trade" +PING_TIMEOUT = 10 + PUBLIC_ENDPOINT_LIMIT_ID = "PublicEndpointLimitID" PUBLIC_ENDPOINT_LIMIT = 1 PUBLIC_ENDPOINT_LIMIT_INTERVAL = 1 diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py new file mode 100644 index 0000000000..449a529935 --- /dev/null +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -0,0 +1,587 @@ +import asyncio +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple + +from bidict import bidict + +from hummingbot.connector.constants import s_decimal_NaN +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_utils, kraken_web_utils as web_utils +from hummingbot.connector.exchange.kraken.kraken_utils import ( + build_api_factory, + build_rate_limits_by_tier, + convert_from_exchange_symbol, + convert_from_exchange_trading_pair, + convert_to_exchange_trading_pair, + is_dark_pool, + split_to_base_quote, +) +from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource +from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource +from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.connector.exchange_py_base import ExchangePyBase +from hummingbot.connector.trading_rule import TradingRule +from hummingbot.connector.utils import TradeFillOrderDetails, combine_to_hb_trading_pair +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +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.order_book_tracker_data_source import OrderBookTrackerDataSource +from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase +from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource +from hummingbot.core.event.events import MarketEvent, OrderFilledEvent +from hummingbot.core.utils.async_utils import safe_gather +from hummingbot.core.web_assistant.connections.data_types import RESTMethod +from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory + +if TYPE_CHECKING: + from hummingbot.client.config.config_helpers import ClientConfigAdapter + + +class KrakenExchange(ExchangePyBase): + UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0 + SHORT_POLL_INTERVAL = 30.0 + + web_utils = web_utils + + def __init__(self, + client_config_map: "ClientConfigAdapter", + kraken_api_key: str, + kraken_api_secret: str, + trading_pairs: Optional[List[str]] = None, + trading_required: bool = True, + domain: str = CONSTANTS.DEFAULT_DOMAIN, + kraken_api_tier: str = "starter" + ): + self.api_key = kraken_api_key + self.secret_key = kraken_api_secret + self._domain = domain + self._trading_required = trading_required + self._trading_pairs = trading_pairs + # todo + self._last_trades_poll_kraken_timestamp = 1.0 + self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) + self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) + super().__init__(client_config_map) + + @staticmethod + def kraken_order_type(order_type: OrderType) -> str: + return order_type.name.upper() + + @staticmethod + def to_hb_order_type(kraken_type: str) -> OrderType: + return OrderType[kraken_type] + + @property + def authenticator(self): + return KrakenAuth( + api_key=self.api_key, + secret_key=self.secret_key, + time_provider=self._time_synchronizer) + + @property + def name(self) -> str: + return "kraken" + # todo + @property + def rate_limits_rules(self): + return CONSTANTS.RATE_LIMITS + + @property + def domain(self): + return self._domain + + @property + def client_order_id_max_length(self): + return CONSTANTS.MAX_ORDER_ID_LEN + + @property + def client_order_id_prefix(self): + return CONSTANTS.HBOT_ORDER_ID_PREFIX + + @property + def trading_rules_request_path(self): + return CONSTANTS.ASSET_PAIRS_PATH_URL + + @property + def trading_pairs_request_path(self): + return CONSTANTS.ASSET_PAIRS_PATH_URL + + @property + def check_network_request_path(self): + return CONSTANTS.TICKER_PATH_URL + + @property + def trading_pairs(self): + return self._trading_pairs + # todo + @property + def is_cancel_request_in_exchange_synchronous(self) -> bool: + return True + + @property + def is_trading_required(self) -> bool: + return self._trading_required + + def supported_order_types(self): + return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] + + # async def get_all_pairs_prices(self) -> List[Dict[str, str]]: + # pairs_prices = await self._api_get(path_url=CONSTANTS.TICKER_BOOK_PATH_URL) + # return pairs_prices + + def _build_async_throttler(self, api_tier: KrakenAPITier) -> AsyncThrottler: + limits_pct = self._client_config.rate_limits_share_pct + if limits_pct < Decimal("100"): + self.logger().warning( + f"The Kraken API does not allow enough bandwidth for a reduced rate-limit share percentage." + f" Current percentage: {limits_pct}." + ) + throttler = AsyncThrottler(build_rate_limits_by_tier(api_tier)) + return throttler + # todo + def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception): + error_description = str(request_exception) + is_time_synchronizer_related = ("-1021" in error_description + and "Timestamp for this request" in error_description) + return is_time_synchronizer_related + + # todo + def _is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool: + return str(CONSTANTS.ORDER_NOT_EXIST_ERROR_CODE) in str( + status_update_exception + ) and CONSTANTS.ORDER_NOT_EXIST_MESSAGE in str(status_update_exception) + + # todo + def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: + return str(CONSTANTS.UNKNOWN_ORDER_ERROR_CODE) in str( + cancelation_exception + ) and CONSTANTS.UNKNOWN_ORDER_MESSAGE in str(cancelation_exception) + + def _create_web_assistants_factory(self) -> WebAssistantsFactory: + return web_utils.build_api_factory( + throttler=self._throttler, + time_synchronizer=self._time_synchronizer, + domain=self._domain, + auth=self._auth) + + def _create_order_book_data_source(self) -> OrderBookTrackerDataSource: + return KrakenAPIOrderBookDataSource( + trading_pairs=self._trading_pairs, + connector=self, + domain=self.domain, + api_factory=self._web_assistants_factory) + + def _create_user_stream_data_source(self) -> UserStreamTrackerDataSource: + return KrakenAPIUserStreamDataSource( + auth=self._auth, + trading_pairs=self._trading_pairs, + connector=self, + api_factory=self._web_assistants_factory, + domain=self.domain, + ) + + def _get_fee(self, + base_currency: str, + quote_currency: str, + order_type: OrderType, + order_side: TradeType, + amount: Decimal, + price: Decimal = s_decimal_NaN, + is_maker: Optional[bool] = None) -> TradeFeeBase: + is_maker = order_type is OrderType.LIMIT_MAKER + return DeductedFromReturnsTradeFee(percent=self.estimate_fee_pct(is_maker)) + + async def _place_order(self, + order_id: str, + trading_pair: str, + amount: Decimal, + trade_type: TradeType, + order_type: OrderType, + price: Decimal, + **kwargs) -> Tuple[str, float]: + order_result = None + amount_str = f"{amount:f}" + type_str = KrakenExchange.kraken_order_type(order_type) + side_str = CONSTANTS.SIDE_BUY if trade_type is TradeType.BUY else CONSTANTS.SIDE_SELL + symbol = await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) + api_params = {"symbol": symbol, + "side": side_str, + "quantity": amount_str, + # "quoteOrderQty": amount_str, + "type": type_str, + "newClientOrderId": order_id} + if order_type.is_limit_type(): + price_str = f"{price:f}" + api_params["price"] = price_str + else: + if trade_type.name.lower() == 'buy': + if price.is_nan(): + price = self.get_price_for_volume( + trading_pair, + True, + amount + ) + del api_params['quantity'] + api_params.update({ + "quoteOrderQty": f"{price * amount:f}", + }) + if order_type == OrderType.LIMIT: + api_params["timeInForce"] = CONSTANTS.TIME_IN_FORCE_GTC + + try: + order_result = await self._api_post( + path_url=CONSTANTS.ORDER_PATH_URL, + data=api_params, + is_auth_required=True) + o_id = str(order_result["orderId"]) + transact_time = order_result["transactTime"] * 1e-3 + except IOError as e: + error_description = str(e) + is_server_overloaded = ("status is 503" in error_description + and "Unknown error, please check your request or try again later." in error_description) + if is_server_overloaded: + o_id = "UNKNOWN" + transact_time = self._time_synchronizer.time() + else: + raise + return o_id, transact_time + + async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): + symbol = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) + api_params = { + "symbol": symbol, + "origClientOrderId": order_id, + } + cancel_result = await self._api_delete( + path_url=CONSTANTS.ORDER_PATH_URL, + params=api_params, + is_auth_required=True) + if cancel_result.get("status") == "NEW": + return True + return False + + async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> List[TradingRule]: + trading_pair_rules = exchange_info_dict.get("symbols", []) + retval = [] + for rule in filter(kraken_utils.is_exchange_information_valid, trading_pair_rules): + try: + trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol=rule.get("symbol")) + min_order_size = Decimal(rule.get("baseSizePrecision")) + min_price_inc = Decimal(f"1e-{rule['quotePrecision']}") + min_amount_inc = Decimal(f"1e-{rule['baseAssetPrecision']}") + min_notional = Decimal(rule['quoteAmountPrecision']) + retval.append( + TradingRule(trading_pair, + min_order_size=min_order_size, + min_price_increment=min_price_inc, + min_base_amount_increment=min_amount_inc, + min_notional_size=min_notional)) + + except Exception: + self.logger().exception(f"Error parsing the trading pair rule {rule}. Skipping.") + return retval + + async def _status_polling_loop_fetch_updates(self): + await self._update_order_fills_from_trades() + await super()._status_polling_loop_fetch_updates() + + async def _update_trading_fees(self): + """ + Update fees information from the exchange + """ + pass + + async def _user_stream_event_listener(self): + """ + Listens to messages from _user_stream_tracker.user_stream queue. + Traders, Orders, and Balance updates from the WS. + """ + user_channels = [ + CONSTANTS.USER_TRADES_ENDPOINT_NAME, + CONSTANTS.USER_ORDERS_ENDPOINT_NAME, + CONSTANTS.USER_BALANCE_ENDPOINT_NAME, + ] + async for event_message in self._iter_user_event_queue(): + try: + channel: str = event_message.get("c", None) + results: Dict[str, Any] = event_message.get("d", {}) + if "code" not in event_message and channel not in user_channels: + self.logger().error( + f"Unexpected message in user stream: {event_message}.", exc_info=True) + continue + if channel == CONSTANTS.USER_TRADES_ENDPOINT_NAME: + self._process_trade_message(results) + elif channel == CONSTANTS.USER_ORDERS_ENDPOINT_NAME: + self._process_order_message(event_message) + elif channel == CONSTANTS.USER_BALANCE_ENDPOINT_NAME: + self._process_balance_message_ws(results) + + except asyncio.CancelledError: + raise + except Exception: + self.logger().error( + "Unexpected error in user stream listener loop.", exc_info=True) + await self._sleep(5.0) + + def _process_balance_message_ws(self, account): + asset_name = account["a"] + self._account_available_balances[asset_name] = Decimal(str(account["f"])) + self._account_balances[asset_name] = Decimal(str(account["f"])) + Decimal(str(account["l"])) + + def _create_trade_update_with_order_fill_data( + self, + order_fill: Dict[str, Any], + order: InFlightOrder): + + fee = TradeFeeBase.new_spot_fee( + fee_schema=self.trade_fee_schema(), + trade_type=order.trade_type, + percent_token=order_fill["N"], + flat_fees=[TokenAmount( + amount=Decimal(order_fill["n"]), + token=order_fill["N"] + )] + ) + trade_update = TradeUpdate( + trade_id=str(order_fill["t"]), + client_order_id=order.client_order_id, + exchange_order_id=order.exchange_order_id, + trading_pair=order.trading_pair, + fee=fee, + fill_base_amount=Decimal(order_fill["v"]), + fill_quote_amount=Decimal(order_fill["a"]), + fill_price=Decimal(order_fill["p"]), + fill_timestamp=order_fill["T"] * 1e-3, + ) + return trade_update + + def _process_trade_message(self, trade: Dict[str, Any], client_order_id: Optional[str] = None): + client_order_id = client_order_id or str(trade["c"]) + tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) + if tracked_order is None: + self.logger().debug(f"Ignoring trade message with id {client_order_id}: not in in_flight_orders.") + else: + trade_update = self._create_trade_update_with_order_fill_data( + order_fill=trade, + order=tracked_order) + self._order_tracker.process_trade_update(trade_update) + + def _create_order_update_with_order_status_data(self, order_status: Dict[str, Any], order: InFlightOrder): + client_order_id = str(order_status["d"].get("c", "")) + order_update = OrderUpdate( + trading_pair=order.trading_pair, + update_timestamp=int(order_status["t"] * 1e-3), + new_state=CONSTANTS.WS_ORDER_STATE[order_status["d"]["s"]], + client_order_id=client_order_id, + exchange_order_id=str(order_status["d"]["i"]), + ) + return order_update + + def _process_order_message(self, raw_msg: Dict[str, Any]): + order_msg = raw_msg.get("d", {}) + client_order_id = str(order_msg.get("c", "")) + tracked_order = self._order_tracker.all_updatable_orders.get(client_order_id) + if not tracked_order: + self.logger().debug(f"Ignoring order message with id {client_order_id}: not in in_flight_orders.") + return + + order_update = self._create_order_update_with_order_status_data(order_status=raw_msg, order=tracked_order) + self._order_tracker.process_order_update(order_update=order_update) + + async def _update_order_fills_from_trades(self): + """ + This is intended to be a backup measure to get filled events with trade ID for orders, + in case Kraken's user stream events are not working. + NOTE: It is not required to copy this functionality in other connectors. + This is separated from _update_order_status which only updates the order status without producing filled + events, since Kraken's get order endpoint does not return trade IDs. + The minimum poll interval for order status is 10 seconds. + """ + small_interval_last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL + small_interval_current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL + long_interval_last_tick = self._last_poll_timestamp / self.LONG_POLL_INTERVAL + long_interval_current_tick = self.current_timestamp / self.LONG_POLL_INTERVAL + + if (long_interval_current_tick > long_interval_last_tick + or (self.in_flight_orders and small_interval_current_tick > small_interval_last_tick)): + query_time = int(self._last_trades_poll_kraken_timestamp * 1e3) + self._last_trades_poll_kraken_timestamp = self._time_synchronizer.time() + order_by_exchange_id_map = {} + for order in self._order_tracker.all_fillable_orders.values(): + order_by_exchange_id_map[order.exchange_order_id] = order + + tasks = [] + trading_pairs = self.trading_pairs + for trading_pair in trading_pairs: + params = { + "symbol": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) + } + if self._last_poll_timestamp > 0: + params["startTime"] = query_time + tasks.append(self._api_get( + path_url=CONSTANTS.MY_TRADES_PATH_URL, + params=params, + is_auth_required=True)) + + self.logger().debug(f"Polling for order fills of {len(tasks)} trading pairs.") + results = await safe_gather(*tasks, return_exceptions=True) + + for trades, trading_pair in zip(results, trading_pairs): + + if isinstance(trades, Exception): + self.logger().network( + f"Error fetching trades update for the order {trading_pair}: {trades}.", + app_warning_msg=f"Failed to fetch trade update for {trading_pair}." + ) + continue + for trade in trades: + exchange_order_id = str(trade["orderId"]) + if exchange_order_id in order_by_exchange_id_map: + # This is a fill for a tracked order + tracked_order = order_by_exchange_id_map[exchange_order_id] + fee = TradeFeeBase.new_spot_fee( + fee_schema=self.trade_fee_schema(), + trade_type=tracked_order.trade_type, + percent_token=trade["commissionAsset"], + flat_fees=[TokenAmount(amount=Decimal(trade["commission"]), token=trade["commissionAsset"])] + ) + trade_update = TradeUpdate( + trade_id=str(trade["id"]), + client_order_id=tracked_order.client_order_id, + exchange_order_id=exchange_order_id, + trading_pair=trading_pair, + fee=fee, + fill_base_amount=Decimal(trade["qty"]), + fill_quote_amount=Decimal(trade["quoteQty"]), + fill_price=Decimal(trade["price"]), + fill_timestamp=trade["time"] * 1e-3, + ) + self._order_tracker.process_trade_update(trade_update) + elif self.is_confirmed_new_order_filled_event(str(trade["id"]), exchange_order_id, trading_pair): + # This is a fill of an order registered in the DB but not tracked any more + self._current_trade_fills.add(TradeFillOrderDetails( + market=self.display_name, + exchange_trade_id=str(trade["id"]), + symbol=trading_pair)) + self.trigger_event( + MarketEvent.OrderFilled, + OrderFilledEvent( + timestamp=float(trade["time"]) * 1e-3, + order_id=self._exchange_order_ids.get(str(trade["orderId"]), None), + trading_pair=trading_pair, + trade_type=TradeType.BUY if trade["isBuyer"] else TradeType.SELL, + order_type=OrderType.LIMIT_MAKER if trade["isMaker"] else OrderType.LIMIT, + price=Decimal(trade["price"]), + amount=Decimal(trade["qty"]), + trade_fee=DeductedFromReturnsTradeFee( + flat_fees=[ + TokenAmount( + trade["commissionAsset"], + Decimal(trade["commission"]) + ) + ] + ), + exchange_trade_id=str(trade["id"]) + )) + self.logger().info(f"Recreating missing trade in TradeFill: {trade}") + + async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: + trade_updates = [] + + if order.exchange_order_id is not None: + exchange_order_id = order.exchange_order_id + trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=order.trading_pair) + all_fills_response = await self._api_get( + path_url=CONSTANTS.MY_TRADES_PATH_URL, + params={ + "symbol": trading_pair, + "orderId": exchange_order_id + }, + is_auth_required=True, + limit_id=CONSTANTS.MY_TRADES_PATH_URL) + + for trade in all_fills_response: + exchange_order_id = str(trade["orderId"]) + fee = TradeFeeBase.new_spot_fee( + fee_schema=self.trade_fee_schema(), + trade_type=order.trade_type, + percent_token=trade["commissionAsset"], + flat_fees=[TokenAmount(amount=Decimal(trade["commission"]), token=trade["commissionAsset"])] + ) + trade_update = TradeUpdate( + trade_id=str(trade["id"]), + client_order_id=order.client_order_id, + exchange_order_id=exchange_order_id, + trading_pair=trading_pair, + fee=fee, + fill_base_amount=Decimal(trade["qty"]), + fill_quote_amount=Decimal(trade["quoteQty"]), + fill_price=Decimal(trade["price"]), + fill_timestamp=trade["time"] * 1e-3, + ) + trade_updates.append(trade_update) + + return trade_updates + + async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: + trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) + updated_order_data = await self._api_get( + path_url=CONSTANTS.ORDER_PATH_URL, + params={ + "symbol": trading_pair, + "origClientOrderId": tracked_order.client_order_id}, + is_auth_required=True) + + new_state = CONSTANTS.ORDER_STATE[updated_order_data["status"]] + + order_update = OrderUpdate( + client_order_id=tracked_order.client_order_id, + exchange_order_id=str(updated_order_data["orderId"]), + trading_pair=tracked_order.trading_pair, + update_timestamp=updated_order_data["updateTime"] * 1e-3, + new_state=new_state, + ) + + return order_update + + async def _update_balances(self): + local_asset_names = set(self._account_balances.keys()) + remote_asset_names = set() + + account_info = await self._api_get( + path_url=CONSTANTS.ACCOUNTS_PATH_URL, + is_auth_required=True) + + balances = account_info["balances"] + for balance_entry in balances: + asset_name = balance_entry["asset"] + free_balance = Decimal(balance_entry["free"]) + total_balance = Decimal(balance_entry["free"]) + Decimal(balance_entry["locked"]) + self._account_available_balances[asset_name] = free_balance + self._account_balances[asset_name] = total_balance + remote_asset_names.add(asset_name) + + asset_names_to_remove = local_asset_names.difference(remote_asset_names) + for asset_name in asset_names_to_remove: + del self._account_available_balances[asset_name] + del self._account_balances[asset_name] + + def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]): + mapping = bidict() + for symbol_data in filter(kraken_utils.is_exchange_information_valid, exchange_info["symbols"]): + mapping[symbol_data["symbol"]] = combine_to_hb_trading_pair(base=symbol_data["baseAsset"], + quote=symbol_data["quoteAsset"]) + self._set_trading_pair_symbol_map(mapping) + + async def _get_last_traded_price(self, trading_pair: str) -> float: + params = { + "symbol": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) + } + + resp_json = await self._api_request( + method=RESTMethod.GET, + path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, + params=params + ) + + return float(resp_json["lastPrice"]) diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.py b/hummingbot/connector/exchange/kraken/kraken_order_book.py new file mode 100644 index 0000000000..801f1afe99 --- /dev/null +++ b/hummingbot/connector/exchange/kraken/kraken_order_book.py @@ -0,0 +1,77 @@ +import logging +from typing import ( + Dict, + Optional +) + +from hummingbot.core.data_type.common import TradeType +from hummingbot.core.data_type.order_book import OrderBook +from hummingbot.core.data_type.order_book_message import ( + OrderBookMessage, + OrderBookMessageType +) + + +class KrakenOrderBook(OrderBook): + + @classmethod + def snapshot_message_from_exchange(cls, + msg: Dict[str, any], + timestamp: float, + metadata: Optional[Dict] = None) -> OrderBookMessage: + if metadata: + msg.update(metadata) + return OrderBookMessage(OrderBookMessageType.SNAPSHOT, { + "trading_pair": msg["trading_pair"].replace("/", ""), + "update_id": msg["latest_update"], + "bids": msg["bids"], + "asks": msg["asks"] + }, timestamp=timestamp * 1e-3) + + @classmethod + def diff_message_from_exchange(cls, + msg: Dict[str, any], + timestamp: Optional[float] = None, + metadata: Optional[Dict] = None) -> OrderBookMessage: + if metadata: + msg.update(metadata) + return OrderBookMessage(OrderBookMessageType.DIFF, { + "trading_pair": msg["trading_pair"].replace("/", ""), + "update_id": msg["update_id"], + "bids": msg["bids"], + "asks": msg["asks"] + }, timestamp=timestamp * 1e-3) + + @classmethod + def snapshot_ws_message_from_exchange(cls, + msg: Dict[str, any], + timestamp: Optional[float] = None, + metadata: Optional[Dict] = None) -> OrderBookMessage: + if metadata: + msg.update(metadata) + return OrderBookMessage(OrderBookMessageType.SNAPSHOT, { + "trading_pair": msg["trading_pair"].replace("/", ""), + "update_id": msg["update_id"], + "bids": msg["bids"], + "asks": msg["asks"] + }, timestamp=timestamp * 1e-3) + + @classmethod + def trade_message_from_exchange(cls, msg: Dict[str, any], metadata: Optional[Dict] = None): + if metadata: + msg.update(metadata) + ts = float(msg["trade"][2]) + return OrderBookMessage(OrderBookMessageType.TRADE, { + "trading_pair": msg["pair"].replace("/", ""), + "trade_type": float(TradeType.SELL.value) if msg["trade"][3] == "s" else float(TradeType.BUY.value), + "trade_id": ts, + "update_id": ts, + "price": msg["trade"][0], + "amount": msg["trade"][1] + }, timestamp=ts * 1e-3) + + @classmethod + def from_snapshot(cls, msg: OrderBookMessage) -> "OrderBook": + retval = KrakenOrderBook() + retval.apply_snapshot(msg.bids, msg.asks, msg.update_id) + return retval diff --git a/hummingbot/connector/exchange/kraken/kraken_web_utils.py b/hummingbot/connector/exchange/kraken/kraken_web_utils.py new file mode 100644 index 0000000000..0a48f7216e --- /dev/null +++ b/hummingbot/connector/exchange/kraken/kraken_web_utils.py @@ -0,0 +1,75 @@ +from typing import Callable, Optional + +import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS +from hummingbot.connector.time_synchronizer import TimeSynchronizer +from hummingbot.connector.utils import TimeSynchronizerRESTPreProcessor +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +from hummingbot.core.web_assistant.auth import AuthBase +from hummingbot.core.web_assistant.connections.data_types import RESTMethod +from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory + + +def public_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> str: + """ + Creates a full URL for provided public REST endpoint + :param path_url: a public REST endpoint + :param domain: the Kraken domain to connect to ("com" or "us"). The default value is "com" + :return: the full URL to the endpoint + """ + return CONSTANTS.REST_URL.format(domain) + CONSTANTS.PUBLIC_API_VERSION + path_url + + +def private_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> str: + """ + Creates a full URL for provided private REST endpoint + :param path_url: a private REST endpoint + :param domain: the Kraken domain to connect to ("com" or "us"). The default value is "com" + :return: the full URL to the endpoint + """ + return CONSTANTS.REST_URL.format(domain) + CONSTANTS.PRIVATE_API_VERSION + path_url + + +def build_api_factory( + throttler: Optional[AsyncThrottler] = None, + time_synchronizer: Optional[TimeSynchronizer] = None, + domain: str = CONSTANTS.DEFAULT_DOMAIN, + time_provider: Optional[Callable] = None, + auth: Optional[AuthBase] = None, ) -> WebAssistantsFactory: + throttler = throttler or create_throttler() + time_synchronizer = time_synchronizer or TimeSynchronizer() + time_provider = time_provider or (lambda: get_current_server_time( + throttler=throttler, + domain=domain, + )) + api_factory = WebAssistantsFactory( + throttler=throttler, + auth=auth, + rest_pre_processors=[ + TimeSynchronizerRESTPreProcessor(synchronizer=time_synchronizer, time_provider=time_provider), + ]) + return api_factory + + +def build_api_factory_without_time_synchronizer_pre_processor(throttler: AsyncThrottler) -> WebAssistantsFactory: + api_factory = WebAssistantsFactory(throttler=throttler) + return api_factory + + +def create_throttler() -> AsyncThrottler: + return AsyncThrottler(CONSTANTS.RATE_LIMITS) + + +async def get_current_server_time( + throttler: Optional[AsyncThrottler] = None, + domain: str = CONSTANTS.DEFAULT_DOMAIN, +) -> float: + throttler = throttler or create_throttler() + api_factory = build_api_factory_without_time_synchronizer_pre_processor(throttler=throttler) + rest_assistant = await api_factory.get_rest_assistant() + response = await rest_assistant.execute_request( + url=public_rest_url(path_url=CONSTANTS.SERVER_TIME_PATH_URL, domain=domain), + method=RESTMethod.GET, + throttler_limit_id=CONSTANTS.SERVER_TIME_PATH_URL, + ) + server_time = response["serverTime"] + return server_time diff --git a/hummingbot/connector/exchange/kraken_v1/__init__.py b/hummingbot/connector/exchange/kraken_v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py new file mode 100755 index 0000000000..7c165667fd --- /dev/null +++ b/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py @@ -0,0 +1,290 @@ +import asyncio +import logging +import time +from typing import Any, Dict, List, Optional + +import pandas as pd + +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook +from hummingbot.connector.exchange.kraken.kraken_utils import ( + build_api_factory, + build_rate_limits_by_tier, + convert_from_exchange_trading_pair, + convert_to_exchange_trading_pair, +) +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +from hummingbot.core.data_type.order_book import OrderBook +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.utils.async_utils import safe_gather +from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest +from hummingbot.core.web_assistant.rest_assistant import RESTAssistant +from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory +from hummingbot.core.web_assistant.ws_assistant import WSAssistant +from hummingbot.logger import HummingbotLogger + + +class KrakenAPIOrderBookDataSource(OrderBookTrackerDataSource): + MESSAGE_TIMEOUT = 30.0 + PING_TIMEOUT = 10.0 + + _kraobds_logger: Optional[HummingbotLogger] = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls._kraobds_logger is None: + cls._kraobds_logger = logging.getLogger(__name__) + return cls._kraobds_logger + + def __init__(self, + throttler: Optional[AsyncThrottler] = None, + trading_pairs: List[str] = None, + api_factory: Optional[WebAssistantsFactory] = None): + super().__init__(trading_pairs) + self._throttler = throttler or self._get_throttler_instance() + self._api_factory = api_factory or build_api_factory(throttler=throttler) + self._rest_assistant = None + self._ws_assistant = None + self._order_book_create_function = lambda: OrderBook() + + @classmethod + def _get_throttler_instance(cls) -> AsyncThrottler: + throttler = AsyncThrottler(build_rate_limits_by_tier()) + return throttler + + async def _get_rest_assistant(self) -> RESTAssistant: + if self._rest_assistant is None: + self._rest_assistant = await self._api_factory.get_rest_assistant() + return self._rest_assistant + + @classmethod + async def get_last_traded_prices( + cls, trading_pairs: List[str], throttler: Optional[AsyncThrottler] = None + ) -> Dict[str, float]: + throttler = throttler or cls._get_throttler_instance() + tasks = [cls._get_last_traded_price(t_pair, throttler) for t_pair in trading_pairs] + results = await safe_gather(*tasks) + return {t_pair: result for t_pair, result in zip(trading_pairs, results)} + + @classmethod + async def _get_last_traded_price(cls, trading_pair: str, throttler: AsyncThrottler) -> float: + url = ( + f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" + f"?pair={convert_to_exchange_trading_pair(trading_pair)}" + ) + + request = RESTRequest( + method=RESTMethod.GET, + url=url + ) + rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() + + async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL): + resp = await rest_assistant.call(request) + resp_json = await resp.json() + record = list(resp_json["result"].values())[0] + return float(record["c"][0]) + + @classmethod + async def get_snapshot( + cls, + rest_assistant: RESTAssistant, + trading_pair: str, + limit: int = 1000, + throttler: Optional[AsyncThrottler] = None, + ) -> Dict[str, Any]: + throttler = throttler or cls._get_throttler_instance() + original_trading_pair: str = trading_pair + if limit != 0: + params = { + "count": str(limit), + "pair": convert_to_exchange_trading_pair(trading_pair) + } + else: + params = {"pair": convert_to_exchange_trading_pair(trading_pair)} + async with throttler.execute_task(CONSTANTS.SNAPSHOT_PATH_URL): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" + + request = RESTRequest( + method=RESTMethod.GET, + url=url, + params=params + ) + + response = await rest_assistant.call(request) + + if response.status != 200: + raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " + f"HTTP status is {response.status}.") + response_json = await response.json() + if len(response_json["error"]) > 0: + raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " + f"Error is {response_json['error']}.") + data: Dict[str, Any] = next(iter(response_json["result"].values())) + data = {"trading_pair": trading_pair, **data} + data["latest_update"] = max([*map(lambda x: x[2], data["bids"] + data["asks"])], default=0.) + + return data + + async def get_new_order_book(self, trading_pair: str) -> OrderBook: + rest_assistant = await self._get_rest_assistant() + snapshot: Dict[str, Any] = await self.get_snapshot( + rest_assistant, trading_pair, limit=1000, throttler=self._throttler + ) + snapshot_timestamp: float = time.time() + snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( + snapshot, + snapshot_timestamp, + metadata={"trading_pair": trading_pair} + ) + order_book: OrderBook = self.order_book_create_function() + order_book.apply_snapshot(snapshot_msg.bids, snapshot_msg.asks, snapshot_msg.update_id) + return order_book + + @classmethod + async def fetch_trading_pairs(cls, throttler: Optional[AsyncThrottler] = None) -> List[str]: + throttler = throttler or cls._get_throttler_instance() + try: + async with throttler.execute_task(CONSTANTS.ASSET_PAIRS_PATH_URL): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + request = RESTRequest( + method=RESTMethod.GET, + url=url + ) + rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() + response = await rest_assistant.call(request, timeout=5) + + if response.status == 200: + data: Dict[str, Any] = await response.json() + raw_pairs = data.get("result", []) + converted_pairs: List[str] = [] + for pair, details in raw_pairs.items(): + if "." not in pair: + try: + wsname = details["wsname"] # pair in format BASE/QUOTE + converted_pairs.append(convert_from_exchange_trading_pair(wsname)) + except IOError: + pass + return [item for item in converted_pairs] + except Exception: + pass + # Do nothing if the request fails -- there will be no autocomplete for kraken trading pairs + return [] + + async def listen_for_trades(self, ev_loop: asyncio.AbstractEventLoop, output: asyncio.Queue): + while True: + try: + ws_message: str = await self.get_ws_subscription_message("trade") + + async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): + ws: WSAssistant = await self._api_factory.get_ws_assistant() + await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) + + await ws.send(ws_message) + async for ws_response in ws.iter_messages(): + msg = ws_response.data + if not (type(msg) is dict and "event" in msg.keys() and + msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): + trades = [ + {"pair": convert_from_exchange_trading_pair(msg[-1]), "trade": trade} + for trade in msg[1] + ] + for trade in trades: + trade_msg: OrderBookMessage = KrakenOrderBook.trade_message_from_exchange(trade) + output.put_nowait(trade_msg) + ws.disconnect() + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", + exc_info=True) + await asyncio.sleep(30.0) + + async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + while True: + try: + ws_message: str = await self.get_ws_subscription_message("book") + async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): + ws: WSAssistant = await self._api_factory.get_ws_assistant() + await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) + + await ws.send(ws_message) + async for ws_response in ws.iter_messages(): + msg = ws_response.data + if not (type(msg) is dict and "event" in msg.keys() and + msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): + msg_dict = {"trading_pair": convert_from_exchange_trading_pair(msg[-1]), + "asks": msg[1].get("a", []) or msg[1].get("as", []) or [], + "bids": msg[1].get("b", []) or msg[1].get("bs", []) or []} + msg_dict["update_id"] = max( + [*map(lambda x: float(x[2]), msg_dict["bids"] + msg_dict["asks"])], default=0. + ) + if "as" in msg[1] and "bs" in msg[1]: + order_book_message: OrderBookMessage = ( + KrakenOrderBook.snapshot_ws_message_from_exchange(msg_dict, time.time()) + ) + else: + order_book_message: OrderBookMessage = KrakenOrderBook.diff_message_from_exchange( + msg_dict, time.time()) + output.put_nowait(order_book_message) + ws.disconnect() + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", + exc_info=True) + await asyncio.sleep(30.0) + + async def listen_for_order_book_snapshots(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + rest_assistant = await self._get_rest_assistant() + while True: + try: + for trading_pair in self._trading_pairs: + try: + snapshot: Dict[str, Any] = await self.get_snapshot( + rest_assistant, trading_pair, throttler=self._throttler + ) + snapshot_timestamp: float = time.time() + snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( + snapshot, + snapshot_timestamp, + metadata={"trading_pair": trading_pair} + ) + output.put_nowait(snapshot_msg) + self.logger().debug(f"Saved order book snapshot for {trading_pair}") + await asyncio.sleep(5.0) + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error. ", exc_info=True) + await asyncio.sleep(5.0) + this_hour: pd.Timestamp = pd.Timestamp.utcnow().replace(minute=0, second=0, microsecond=0) + next_hour: pd.Timestamp = this_hour + pd.Timedelta(hours=1) + delta: float = next_hour.timestamp() - time.time() + await asyncio.sleep(delta) + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error. ", exc_info=True) + await asyncio.sleep(5.0) + + async def get_ws_subscription_message(self, subscription_type: str): + trading_pairs: List[str] = [] + for tp in self._trading_pairs: + trading_pairs.append(convert_to_exchange_trading_pair(tp, '/')) + + ws_message: WSJSONRequest = WSJSONRequest({ + "event": "subscribe", + "pair": trading_pairs, + "subscription": {"name": subscription_type, "depth": 1000}}) + + return ws_message + + async def listen_for_subscriptions(self): + """ + Connects to the trade events and order diffs websocket endpoints and listens to the messages sent by the + exchange. Each message is stored in its own queue. + """ + # This connector does not use this base class method and needs a refactoring + pass diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py new file mode 100755 index 0000000000..c3484c2d7e --- /dev/null +++ b/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py @@ -0,0 +1,126 @@ +import asyncio +import logging +from typing import Any, Dict, Optional + +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook +from hummingbot.connector.exchange.kraken.kraken_utils import build_api_factory +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource +from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest +from hummingbot.core.web_assistant.rest_assistant import RESTAssistant +from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory +from hummingbot.core.web_assistant.ws_assistant import WSAssistant +from hummingbot.logger import HummingbotLogger + +MESSAGE_TIMEOUT = 3.0 +PING_TIMEOUT = 5.0 + + +class KrakenAPIUserStreamDataSource(UserStreamTrackerDataSource): + + _krausds_logger: Optional[HummingbotLogger] = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls._krausds_logger is None: + cls._krausds_logger = logging.getLogger(__name__) + return cls._krausds_logger + + def __init__(self, + throttler: AsyncThrottler, + kraken_auth: KrakenAuth, + api_factory: Optional[WebAssistantsFactory] = None): + self._throttler = throttler + self._api_factory = api_factory or build_api_factory(throttler=throttler) + self._rest_assistant = None + self._ws_assistant = None + self._kraken_auth: KrakenAuth = kraken_auth + self._current_auth_token: Optional[str] = None + super().__init__() + + @property + def order_book_class(self): + return KrakenOrderBook + + @property + def last_recv_time(self): + if self._ws_assistant is None: + return 0 + else: + return self._ws_assistant.last_recv_time + + async def _get_rest_assistant(self) -> RESTAssistant: + if self._rest_assistant is None: + self._rest_assistant = await self._api_factory.get_rest_assistant() + return self._rest_assistant + + async def get_auth_token(self) -> str: + api_auth: Dict[str, Any] = self._kraken_auth.generate_auth_dict(uri=CONSTANTS.GET_TOKEN_PATH_URL) + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" + + request = RESTRequest( + method=RESTMethod.POST, + url=url, + headers=api_auth["headers"], + data=api_auth["postDict"] + ) + rest_assistant = await self._get_rest_assistant() + + async with self._throttler.execute_task(CONSTANTS.GET_TOKEN_PATH_URL): + response = await rest_assistant.call(request=request, timeout=100) + if response.status != 200: + raise IOError(f"Error fetching Kraken user stream listen key. HTTP status is {response.status}.") + + try: + response_json: Dict[str, Any] = await response.json() + except Exception: + raise IOError(f"Error parsing data from {url}.") + + err = response_json["error"] + if "EAPI:Invalid nonce" in err: + self.logger().error(f"Invalid nonce error from {url}. " + + "Please ensure your Kraken API key nonce window is at least 10, " + + "and if needed reset your API key.") + raise IOError({"error": response_json}) + + return response_json["result"]["token"] + + async def listen_for_user_stream(self, output: asyncio.Queue): + ws = None + while True: + try: + async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): + ws: WSAssistant = await self._api_factory.get_ws_assistant() + await ws.connect(ws_url=CONSTANTS.WS_AUTH_URL, ping_timeout=PING_TIMEOUT) + + if self._current_auth_token is None: + self._current_auth_token = await self.get_auth_token() + + for subscription_type in ["openOrders", "ownTrades"]: + subscribe_request: WSJSONRequest = WSJSONRequest({ + "event": "subscribe", + "subscription": { + "name": subscription_type, + "token": self._current_auth_token + } + }) + await ws.send(subscribe_request) + + async for ws_response in ws.iter_messages(): + msg = ws_response.data + if not (type(msg) is dict and "event" in msg.keys() and + msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): + output.put_nowait(msg) + except asyncio.CancelledError: + raise + except Exception: + self.logger().error("Unexpected error with Kraken WebSocket connection. " + "Retrying after 30 seconds...", exc_info=True) + self._current_auth_token = None + await asyncio.sleep(30.0) + finally: + if ws is not None: + await ws.disconnect() diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_auth.py b/hummingbot/connector/exchange/kraken_v1/kraken_auth.py new file mode 100755 index 0000000000..1359d6d8a3 --- /dev/null +++ b/hummingbot/connector/exchange/kraken_v1/kraken_auth.py @@ -0,0 +1,49 @@ +from typing import ( + Optional, + Dict, + Any +) +import base64 +import hashlib +import hmac +from hummingbot.connector.exchange.kraken.kraken_tracking_nonce import get_tracking_nonce + + +class KrakenAuth: + def __init__(self, api_key: str, secret_key: str): + self.api_key = api_key + self.secret_key = secret_key + + def generate_auth_dict(self, uri: str, data: Optional[Dict[str, str]] = None) -> Dict[str, Any]: + """ + Generates authentication signature and returns it in a dictionary + :return: a dictionary of request info including the request signature and post data + """ + + # Decode API private key from base64 format displayed in account management + api_secret: bytes = base64.b64decode(self.secret_key) + + # Variables (API method, nonce, and POST data) + api_path: bytes = bytes(uri, 'utf-8') + api_nonce: str = get_tracking_nonce() + api_post: str = "nonce=" + api_nonce + + if data is not None: + for key, value in data.items(): + api_post += f"&{key}={value}" + + # Cryptographic hash algorithms + api_sha256: bytes = hashlib.sha256(bytes(api_nonce + api_post, 'utf-8')).digest() + api_hmac: hmac.HMAC = hmac.new(api_secret, api_path + api_sha256, hashlib.sha512) + + # Encode signature into base64 format used in API-Sign value + api_signature: bytes = base64.b64encode(api_hmac.digest()) + + return { + "headers": { + "API-Key": self.api_key, + "API-Sign": str(api_signature, 'utf-8') + }, + "post": api_post, + "postDict": {"nonce": api_nonce, **data} if data is not None else {"nonce": api_nonce} + } diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_constants.py b/hummingbot/connector/exchange/kraken_v1/kraken_constants.py new file mode 100644 index 0000000000..f30dca5323 --- /dev/null +++ b/hummingbot/connector/exchange/kraken_v1/kraken_constants.py @@ -0,0 +1,100 @@ +from enum import Enum +from typing import ( + Dict, + Tuple, +) +from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair + + +class KrakenAPITier(Enum): + """ + Kraken's Private Endpoint Rate Limit Tiers, based on the Account Verification level. + """ + STARTER = "STARTER" + INTERMEDIATE = "INTERMEDIATE" + PRO = "PRO" + + +# Values are calculated by adding the Maxiumum Counter value and the expected count decay(in a minute) of a given tier. +# Reference: +# - API Rate Limits: https://support.kraken.com/hc/en-us/articles/206548367-What-are-the-API-rate-limits +# - Matching Engine Limits: https://support.kraken.com/hc/en-us/articles/360045239571 +STARTER_PRIVATE_ENDPOINT_LIMIT = 15 + 20 +STARTER_MATCHING_ENGINE_LIMIT = 60 + 60 +INTERMEDIATE_PRIVATE_ENDPOINT_LIMIT = 20 + 30 +INTERMEDIATE_MATCHING_ENGINE_LIMIT = 125 + 140 +PRO_PRIVATE_ENDPOINT_LIMIT = 20 + 60 +PRO_MATCHING_ENGINE_LIMIT = 180 + 225 + +KRAKEN_TIER_LIMITS: Dict[KrakenAPITier, Tuple[int, int]] = { + KrakenAPITier.STARTER: (STARTER_PRIVATE_ENDPOINT_LIMIT, STARTER_MATCHING_ENGINE_LIMIT), + KrakenAPITier.INTERMEDIATE: (INTERMEDIATE_PRIVATE_ENDPOINT_LIMIT, INTERMEDIATE_MATCHING_ENGINE_LIMIT), + KrakenAPITier.PRO: (PRO_PRIVATE_ENDPOINT_LIMIT, PRO_MATCHING_ENGINE_LIMIT), +} + +KRAKEN_TO_HB_MAP = { + "XBT": "BTC", + "XDG": "DOGE", +} + +BASE_URL = "https://api.kraken.com" +TICKER_PATH_URL = "/0/public/Ticker" +SNAPSHOT_PATH_URL = "/0/public/Depth" +ASSET_PAIRS_PATH_URL = "/0/public/AssetPairs" +TIME_PATH_URL = "/0/public/Time" +GET_TOKEN_PATH_URL = "/0/private/GetWebSocketsToken" +ADD_ORDER_PATH_URL = "/0/private/AddOrder" +CANCEL_ORDER_PATH_URL = "/0/private/CancelOrder" +BALANCE_PATH_URL = "/0/private/Balance" +OPEN_ORDERS_PATH_URL = "/0/private/OpenOrders" +QUERY_ORDERS_PATH_URL = "/0/private/QueryOrders" + +WS_URL = "wss://ws.kraken.com" +WS_AUTH_URL = "wss://ws-auth.kraken.com/" + +PUBLIC_ENDPOINT_LIMIT_ID = "PublicEndpointLimitID" +PUBLIC_ENDPOINT_LIMIT = 1 +PUBLIC_ENDPOINT_LIMIT_INTERVAL = 1 +PRIVATE_ENDPOINT_LIMIT_ID = "PrivateEndpointLimitID" +PRIVATE_ENDPOINT_LIMIT_INTERVAL = 60 +MATCHING_ENGINE_LIMIT_ID = "MatchingEngineLimitID" +MATCHING_ENGINE_LIMIT_INTERVAL = 60 +WS_CONNECTION_LIMIT_ID = "WSConnectionLimitID" + +PUBLIC_API_LIMITS = [ + # Public API Pool + RateLimit( + limit_id=PUBLIC_ENDPOINT_LIMIT_ID, + limit=PUBLIC_ENDPOINT_LIMIT, + time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, + ), + # Public Endpoints + RateLimit( + limit_id=SNAPSHOT_PATH_URL, + limit=PUBLIC_ENDPOINT_LIMIT, + time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=ASSET_PAIRS_PATH_URL, + limit=PUBLIC_ENDPOINT_LIMIT, + time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=TICKER_PATH_URL, + limit=PUBLIC_ENDPOINT_LIMIT, + time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=TIME_PATH_URL, + limit=PUBLIC_ENDPOINT_LIMIT, + time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], + ), + # WebSocket Connection Limit + RateLimit(limit_id=WS_CONNECTION_LIMIT_ID, + limit=150, + time_interval=60 * 10), +] diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_exchange.pxd rename to hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_exchange.pyx rename to hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx diff --git a/hummingbot/connector/exchange/kraken/kraken_in_flight_order.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_in_flight_order.pxd rename to hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd diff --git a/hummingbot/connector/exchange/kraken/kraken_in_flight_order.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_in_flight_order.pyx rename to hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_order_book.pxd rename to hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_order_book.pyx rename to hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py b/hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py rename to hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py diff --git a/hummingbot/connector/exchange/kraken/kraken_tracking_nonce.py b/hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_tracking_nonce.py rename to hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py diff --git a/hummingbot/connector/exchange/kraken/kraken_user_stream_tracker.py b/hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py similarity index 100% rename from hummingbot/connector/exchange/kraken/kraken_user_stream_tracker.py rename to hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_utils.py b/hummingbot/connector/exchange/kraken_v1/kraken_utils.py new file mode 100644 index 0000000000..d00c27842b --- /dev/null +++ b/hummingbot/connector/exchange/kraken_v1/kraken_utils.py @@ -0,0 +1,217 @@ +from typing import Any, Dict, List, Optional, Tuple + +from pydantic import Field, SecretStr + +import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS +from hummingbot.client.config.config_data_types import BaseConnectorConfigMap, ClientFieldData +from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit +from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory + +CENTRALIZED = True + +EXAMPLE_PAIR = "ETH-USDC" + +DEFAULT_FEES = [0.16, 0.26] + + +def split_trading_pair(trading_pair: str) -> Tuple[str, str]: + return tuple(convert_from_exchange_trading_pair(trading_pair).split("-")) + + +def convert_from_exchange_symbol(symbol: str) -> str: + # Assuming if starts with Z or X and has 4 letters then Z/X is removable + if (symbol[0] == "X" or symbol[0] == "Z") and len(symbol) == 4: + symbol = symbol[1:] + return CONSTANTS.KRAKEN_TO_HB_MAP.get(symbol, symbol) + + +def convert_to_exchange_symbol(symbol: str) -> str: + inverted_kraken_to_hb_map = {v: k for k, v in CONSTANTS.KRAKEN_TO_HB_MAP.items()} + return inverted_kraken_to_hb_map.get(symbol, symbol) + + +def split_to_base_quote(exchange_trading_pair: str) -> Tuple[Optional[str], Optional[str]]: + base, quote = exchange_trading_pair.split("-") + return base, quote + + +def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_trading_pairs: Optional[Tuple] = None) -> Optional[str]: + base, quote = "", "" + if "-" in exchange_trading_pair: + base, quote = split_to_base_quote(exchange_trading_pair) + elif "/" in exchange_trading_pair: + base, quote = exchange_trading_pair.split("/") + elif len(available_trading_pairs) > 0: + # If trading pair has no spaces (i.e. ETHUSDT). Then it will have to match with the existing pairs + # Option 1: Using traditional naming convention + connector_trading_pair = {''.join(convert_from_exchange_trading_pair(tp).split('-')): tp for tp in available_trading_pairs}.get( + exchange_trading_pair) + if not connector_trading_pair: + # Option 2: Using kraken naming convention ( XXBT for Bitcoin, XXDG for Doge, ZUSD for USD, etc) + connector_trading_pair = {''.join(tp.split('-')): tp for tp in available_trading_pairs}.get( + exchange_trading_pair) + if not connector_trading_pair: + # Option 3: Kraken naming convention but without the initial X and Z + connector_trading_pair = {''.join(convert_to_exchange_symbol(convert_from_exchange_symbol(s)) + for s in tp.split('-')): tp + for tp in available_trading_pairs}.get(exchange_trading_pair) + return connector_trading_pair + + if not base or not quote: + return None + base = convert_from_exchange_symbol(base) + quote = convert_from_exchange_symbol(quote) + return f"{base}-{quote}" + + +def convert_to_exchange_trading_pair(hb_trading_pair: str, delimiter: str = "") -> str: + """ + Note: The result of this method can safely be used to submit/make queries. + Result shouldn't be used to parse responses as Kraken add special formating to most pairs. + """ + if "-" in hb_trading_pair: + base, quote = hb_trading_pair.split("-") + elif "/" in hb_trading_pair: + base, quote = hb_trading_pair.split("/") + else: + return hb_trading_pair + base = convert_to_exchange_symbol(base) + quote = convert_to_exchange_symbol(quote) + + exchange_trading_pair = f"{base}{delimiter}{quote}" + return exchange_trading_pair + + +def is_dark_pool(trading_pair_details: Dict[str, Any]): + ''' + Want to filter out dark pool trading pairs from the list of trading pairs + For more info, please check + https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool + ''' + if trading_pair_details.get('altname'): + return trading_pair_details.get('altname').endswith('.d') + return False + + +def _build_private_rate_limits(tier: KrakenAPITier = KrakenAPITier.STARTER) -> List[RateLimit]: + private_rate_limits = [] + + PRIVATE_ENDPOINT_LIMIT, MATCHING_ENGINE_LIMIT = CONSTANTS.KRAKEN_TIER_LIMITS[tier] + + # Private REST endpoints + private_rate_limits.extend([ + # Private API Pool + RateLimit( + limit_id=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + ), + # Private endpoints + RateLimit( + limit_id=CONSTANTS.GET_TOKEN_PATH_URL, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=CONSTANTS.BALANCE_PATH_URL, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + weight=2, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=CONSTANTS.OPEN_ORDERS_PATH_URL, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + weight=2, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], + ), + RateLimit( + limit_id=CONSTANTS.QUERY_ORDERS_PATH_URL, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + weight=2, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], + ), + ]) + + # Matching Engine Limits + private_rate_limits.extend([ + RateLimit( + limit_id=CONSTANTS.ADD_ORDER_PATH_URL, + limit=MATCHING_ENGINE_LIMIT, + time_interval=CONSTANTS.MATCHING_ENGINE_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.MATCHING_ENGINE_LIMIT_ID)], + ), + RateLimit( + limit_id=CONSTANTS.CANCEL_ORDER_PATH_URL, + limit=MATCHING_ENGINE_LIMIT, + time_interval=CONSTANTS.MATCHING_ENGINE_LIMIT_INTERVAL, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.MATCHING_ENGINE_LIMIT_ID)], + ), + ]) + + return private_rate_limits + + +def build_rate_limits_by_tier(tier: KrakenAPITier = KrakenAPITier.STARTER) -> List[RateLimit]: + rate_limits = [] + + rate_limits.extend(CONSTANTS.PUBLIC_API_LIMITS) + rate_limits.extend(_build_private_rate_limits(tier=tier)) + + return rate_limits + + +def _api_tier_validator(value: str) -> Optional[str]: + """ + Determines if input value is a valid API tier + """ + try: + KrakenAPITier(value.upper()) + except ValueError: + return "No such Kraken API Tier." + + +class KrakenConfigMap(BaseConnectorConfigMap): + connector: str = Field(default="kraken", client_data=None) + kraken_api_key: SecretStr = Field( + default=..., + client_data=ClientFieldData( + prompt=lambda cm: "Enter your Kraken API key", + is_secure=True, + is_connect_key=True, + prompt_on_new=True, + ) + ) + kraken_secret_key: SecretStr = Field( + default=..., + client_data=ClientFieldData( + prompt=lambda cm: "Enter your Kraken secret key", + is_secure=True, + is_connect_key=True, + prompt_on_new=True, + ) + ) + kraken_api_tier: str = Field( + default="Starter", + client_data=ClientFieldData( + prompt=lambda cm: "Enter your Kraken API Tier (Starter/Intermediate/Pro)", + is_connect_key=True, + prompt_on_new=True, + ) + ) + + class Config: + title = "kraken" + + +KEYS = KrakenConfigMap.construct() + + +def build_api_factory(throttler: AsyncThrottler) -> WebAssistantsFactory: + api_factory = WebAssistantsFactory(throttler=throttler) + return api_factory From 57700831bb2e22d0064d382ddb586aa6df5a65b6 Mon Sep 17 00:00:00 2001 From: bczhang Date: Wed, 17 Jan 2024 20:53:06 +0800 Subject: [PATCH 02/34] finish kraken_stream_data_source --- .../kraken_api_user_stream_data_source.py | 183 ++++++++---------- .../connector/exchange/kraken/kraken_auth.py | 43 +++- .../exchange/kraken/kraken_constants.py | 2 + .../exchange/kraken/kraken_exchange.py | 9 +- 4 files changed, 128 insertions(+), 109 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index c3484c2d7e..c89158af64 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -1,48 +1,38 @@ import asyncio -import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, TYPE_CHECKING from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook -from hummingbot.connector.exchange.kraken.kraken_utils import build_api_factory -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest -from hummingbot.core.web_assistant.rest_assistant import RESTAssistant +from hummingbot.core.web_assistant.connections.data_types import WSJSONRequest from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory from hummingbot.core.web_assistant.ws_assistant import WSAssistant from hummingbot.logger import HummingbotLogger -MESSAGE_TIMEOUT = 3.0 -PING_TIMEOUT = 5.0 +if TYPE_CHECKING: + from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange -class KrakenAPIUserStreamDataSource(UserStreamTrackerDataSource): - - _krausds_logger: Optional[HummingbotLogger] = None - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._krausds_logger is None: - cls._krausds_logger = logging.getLogger(__name__) - return cls._krausds_logger +class KrakenAPIUserStreamDataSource(UserStreamTrackerDataSource): + _logger: Optional[HummingbotLogger] = None def __init__(self, - throttler: AsyncThrottler, - kraken_auth: KrakenAuth, + auth: KrakenAuth, + connector: 'KrakenExchange', api_factory: Optional[WebAssistantsFactory] = None): - self._throttler = throttler - self._api_factory = api_factory or build_api_factory(throttler=throttler) - self._rest_assistant = None - self._ws_assistant = None - self._kraken_auth: KrakenAuth = kraken_auth + + self._api_factory = api_factory + self._kraken_auth: KrakenAuth = auth + self._connector = connector self._current_auth_token: Optional[str] = None super().__init__() - @property - def order_book_class(self): - return KrakenOrderBook + async def _connected_websocket_assistant(self) -> WSAssistant: + ws: WSAssistant = await self._api_factory.get_ws_assistant() + await ws.connect(ws_url=CONSTANTS.WS_AUTH_URL, ping_timeout=CONSTANTS.PING_TIMEOUT) + return ws + @property def last_recv_time(self): @@ -51,76 +41,73 @@ def last_recv_time(self): else: return self._ws_assistant.last_recv_time - async def _get_rest_assistant(self) -> RESTAssistant: - if self._rest_assistant is None: - self._rest_assistant = await self._api_factory.get_rest_assistant() - return self._rest_assistant async def get_auth_token(self) -> str: - api_auth: Dict[str, Any] = self._kraken_auth.generate_auth_dict(uri=CONSTANTS.GET_TOKEN_PATH_URL) - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" - - request = RESTRequest( - method=RESTMethod.POST, - url=url, - headers=api_auth["headers"], - data=api_auth["postDict"] - ) - rest_assistant = await self._get_rest_assistant() - - async with self._throttler.execute_task(CONSTANTS.GET_TOKEN_PATH_URL): - response = await rest_assistant.call(request=request, timeout=100) - if response.status != 200: - raise IOError(f"Error fetching Kraken user stream listen key. HTTP status is {response.status}.") - - try: - response_json: Dict[str, Any] = await response.json() - except Exception: - raise IOError(f"Error parsing data from {url}.") - - err = response_json["error"] - if "EAPI:Invalid nonce" in err: - self.logger().error(f"Invalid nonce error from {url}. " + - "Please ensure your Kraken API key nonce window is at least 10, " + - "and if needed reset your API key.") - raise IOError({"error": response_json}) - - return response_json["result"]["token"] - - async def listen_for_user_stream(self, output: asyncio.Queue): - ws = None - while True: - try: - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_AUTH_URL, ping_timeout=PING_TIMEOUT) - - if self._current_auth_token is None: - self._current_auth_token = await self.get_auth_token() - - for subscription_type in ["openOrders", "ownTrades"]: - subscribe_request: WSJSONRequest = WSJSONRequest({ - "event": "subscribe", - "subscription": { - "name": subscription_type, - "token": self._current_auth_token - } - }) - await ws.send(subscribe_request) - - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - output.put_nowait(msg) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with Kraken WebSocket connection. " - "Retrying after 30 seconds...", exc_info=True) - self._current_auth_token = None - await asyncio.sleep(30.0) - finally: - if ws is not None: - await ws.disconnect() + try: + response_json = await self._connector._api_post(path_url=CONSTANTS.GET_TOKEN_PATH_URL, params={}, + is_auth_required=True) + except Exception: + raise IOError(f"Error parsing data from {CONSTANTS.GET_TOKEN_PATH_URL}.") + + err = response_json["error"] + if "EAPI:Invalid nonce" in err: + self.logger().error(f"Invalid nonce error from {CONSTANTS.GET_TOKEN_PATH_URL}. " + + "Please ensure your Kraken API key nonce window is at least 10, " + + "and if needed reset your API key.") + raise IOError({"error": response_json}) + + return response_json["result"]["token"] + + + async def _subscribe_channels(self, websocket_assistant: WSAssistant): + """ + Subscribes to order events and balance events. + + :param websocket_assistant: the websocket assistant used to connect to the exchange + """ + try: + + if self._current_auth_token is None: + self._current_auth_token = await self.get_auth_token() + + orders_change_payload = { + "event": "subscribe", + "subscription": { + "name": "openOrders", + "token": self._current_auth_token + } + } + subscribe_order_change_request: WSJSONRequest = WSJSONRequest(payload=orders_change_payload) + + trades_payload = { + "event": "subscribe", + "subscription": { + "name": "ownTrades", + "token": self._current_auth_token + } + } + subscribe_trades_request: WSJSONRequest = WSJSONRequest(payload=trades_payload) + + await websocket_assistant.send(subscribe_order_change_request) + await websocket_assistant.send(subscribe_trades_request) + + self.logger().info("Subscribed to private order changes and trades updates channels...") + except asyncio.CancelledError: + raise + except Exception: + self.logger().exception("Unexpected error occurred subscribing to user streams...") + raise + + async def _process_event_message(self, event_message: Dict[str, Any], queue: asyncio.Queue): + if type(event_message) is list and event_message[-2] in [ + CONSTANTS.USER_TRADES_ENDPOINT_NAME, + CONSTANTS.USER_ORDERS_ENDPOINT_NAME, + ]: + queue.put_nowait(event_message) + else: + if event_message.get("errorMessage") is not None: + err_msg = event_message.get("errorMessage") + raise IOError({ + "label": "WSS_ERROR", + "message": f"Error received via websocket - {err_msg}." + }) diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index 1359d6d8a3..68bfba080a 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -6,15 +6,48 @@ import base64 import hashlib import hmac -from hummingbot.connector.exchange.kraken.kraken_tracking_nonce import get_tracking_nonce +import time +from hummingbot.connector.time_synchronizer import TimeSynchronizer +from hummingbot.core.web_assistant.auth import AuthBase +from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSRequest -class KrakenAuth: - def __init__(self, api_key: str, secret_key: str): + + + + +class KrakenAuth(AuthBase): + _last_tracking_nonce: int = 0 + + def __init__(self, api_key: str, secret_key: str, time_provider: TimeSynchronizer): self.api_key = api_key self.secret_key = secret_key + self.time_provider = time_provider + + @classmethod + def get_tracking_nonce(self) -> str: + nonce = int(time.time()) + self._last_tracking_nonce = nonce if nonce > self._last_tracking_nonce else self._last_tracking_nonce + 1 + return str(self._last_tracking_nonce) + + async def rest_authenticate(self, request: RESTRequest) -> RESTRequest: + headers = {} + if request.headers is not None: + headers.update(request.headers) + auth_dict: Dict[str, Any] = self._generate_auth_dict(request.url, request.data) + headers.update(auth_dict["headers"]) + request.headers = headers + request.data = auth_dict["postDict"] + return request + #todo + async def ws_authenticate(self, request: WSRequest) -> WSRequest: + """ + This method is intended to configure a websocket request to be authenticated. Mexc does not use this + functionality + """ + return request # pass-through - def generate_auth_dict(self, uri: str, data: Optional[Dict[str, str]] = None) -> Dict[str, Any]: + def _generate_auth_dict(self, uri: str, data: Optional[Dict[str, str]] = None) -> Dict[str, Any]: """ Generates authentication signature and returns it in a dictionary :return: a dictionary of request info including the request signature and post data @@ -25,7 +58,7 @@ def generate_auth_dict(self, uri: str, data: Optional[Dict[str, str]] = None) -> # Variables (API method, nonce, and POST data) api_path: bytes = bytes(uri, 'utf-8') - api_nonce: str = get_tracking_nonce() + api_nonce: str = self.get_tracking_nonce() api_post: str = "nonce=" + api_nonce if data is not None: diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index e170d87774..d19e5ee74a 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -58,6 +58,8 @@ class KrakenAPITier(Enum): DIFF_EVENT_TYPE = "book" TRADE_EVENT_TYPE = "trade" PING_TIMEOUT = 10 +USER_TRADES_ENDPOINT_NAME = "ownTrades" +USER_ORDERS_ENDPOINT_NAME = "openOrders" PUBLIC_ENDPOINT_LIMIT_ID = "PublicEndpointLimitID" PUBLIC_ENDPOINT_LIMIT = 1 diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 449a529935..f945c88e90 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -82,9 +82,9 @@ def authenticator(self): def name(self) -> str: return "kraken" # todo - @property - def rate_limits_rules(self): - return CONSTANTS.RATE_LIMITS + # @property + # def rate_limits_rules(self): + # return CONSTANTS.RATE_LIMITS @property def domain(self): @@ -168,16 +168,13 @@ def _create_order_book_data_source(self) -> OrderBookTrackerDataSource: return KrakenAPIOrderBookDataSource( trading_pairs=self._trading_pairs, connector=self, - domain=self.domain, api_factory=self._web_assistants_factory) def _create_user_stream_data_source(self) -> UserStreamTrackerDataSource: return KrakenAPIUserStreamDataSource( auth=self._auth, - trading_pairs=self._trading_pairs, connector=self, api_factory=self._web_assistants_factory, - domain=self.domain, ) def _get_fee(self, From 558296a761946b0227d6922d3b3d491118eb2d7b Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 25 Jan 2024 19:48:45 +0800 Subject: [PATCH 03/34] , --- .../kraken_api_user_stream_data_source.py | 2 - .../exchange/kraken/kraken_exchange.py | 38 ++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index c89158af64..918589dafa 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -18,12 +18,10 @@ class KrakenAPIUserStreamDataSource(UserStreamTrackerDataSource): _logger: Optional[HummingbotLogger] = None def __init__(self, - auth: KrakenAuth, connector: 'KrakenExchange', api_factory: Optional[WebAssistantsFactory] = None): self._api_factory = api_factory - self._kraken_auth: KrakenAuth = auth self._connector = connector self._current_auth_token: Optional[str] = None super().__init__() diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index f945c88e90..531cdae820 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -5,7 +5,8 @@ from bidict import bidict from hummingbot.connector.constants import s_decimal_NaN -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_utils, kraken_web_utils as web_utils +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_utils, \ + kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_utils import ( build_api_factory, build_rate_limits_by_tier, @@ -81,6 +82,7 @@ def authenticator(self): @property def name(self) -> str: return "kraken" + # todo # @property # def rate_limits_rules(self): @@ -113,6 +115,7 @@ def check_network_request_path(self): @property def trading_pairs(self): return self._trading_pairs + # todo @property def is_cancel_request_in_exchange_synchronous(self) -> bool: @@ -138,6 +141,7 @@ def _build_async_throttler(self, api_tier: KrakenAPITier) -> AsyncThrottler: ) throttler = AsyncThrottler(build_rate_limits_by_tier(api_tier)) return throttler + # todo def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception): error_description = str(request_exception) @@ -172,7 +176,6 @@ def _create_order_book_data_source(self) -> OrderBookTrackerDataSource: def _create_user_stream_data_source(self) -> UserStreamTrackerDataSource: return KrakenAPIUserStreamDataSource( - auth=self._auth, connector=self, api_factory=self._web_assistants_factory, ) @@ -188,6 +191,37 @@ def _get_fee(self, is_maker = order_type is OrderType.LIMIT_MAKER return DeductedFromReturnsTradeFee(percent=self.estimate_fee_pct(is_maker)) + async def place_order(self, + userref: int, + trading_pair: str, + amount: Decimal, + order_type: OrderType, + is_buy: bool, + price: Optional[Decimal] = s_decimal_NaN): + + trading_pair = convert_to_exchange_trading_pair(trading_pair) + data = { + "pair": trading_pair, + "type": "buy" if is_buy else "sell", + "ordertype": "market" if order_type is OrderType.MARKET else "limit", + "volume": str(amount), + "userref": userref, + "price": str(price) + } + if order_type is OrderType.LIMIT_MAKER: + data["oflags"] = "post" + order_result = await self._api_post(path_url=CONSTANTS.ADD_ORDER_PATH_URL, + data=data, + is_auth_required=True) + + # todo + # o_order_result = order_result['response']["data"]["statuses"][0] + # if "error" in o_order_result: + # raise IOError(f"Error submitting order {userref}: {o_order_result['error']}") + # o_data = o_order_result.get("resting") or o_order_result.get("filled") + o_id = order_result["txid"][0] + return (o_id, self.current_timestamp) + async def _place_order(self, order_id: str, trading_pair: str, From 8e6c09a731df7ebc82fab46718a71b44eb97b129 Mon Sep 17 00:00:00 2001 From: bczhang Date: Fri, 26 Jan 2024 20:12:12 +0800 Subject: [PATCH 04/34] , --- .../exchange/kraken/kraken_exchange.py | 245 ++++++++--- .../exchange/kraken/kraken_in_fight_order.py | 380 ++++++++++++++++++ 2 files changed, 560 insertions(+), 65 deletions(-) create mode 100644 hummingbot/connector/exchange/kraken/kraken_in_fight_order.py diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 531cdae820..e90fce2701 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -22,7 +22,7 @@ from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import TradeFillOrderDetails, combine_to_hb_trading_pair +from hummingbot.connector.utils import TradeFillOrderDetails, combine_to_hb_trading_pair, get_new_client_order_id from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderUpdate, TradeUpdate @@ -30,7 +30,8 @@ from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.core.event.events import MarketEvent, OrderFilledEvent -from hummingbot.core.utils.async_utils import safe_gather +from hummingbot.core.utils.async_utils import safe_gather, safe_ensure_future +from hummingbot.core.utils.tracking_nonce import get_tracking_nonce from hummingbot.core.web_assistant.connections.data_types import RESTMethod from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -62,6 +63,8 @@ def __init__(self, self._last_trades_poll_kraken_timestamp = 1.0 self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) + self._last_userref = 0 + super().__init__(client_config_map) @staticmethod @@ -191,18 +194,96 @@ def _get_fee(self, is_maker = order_type is OrderType.LIMIT_MAKER return DeductedFromReturnsTradeFee(percent=self.estimate_fee_pct(is_maker)) - async def place_order(self, - userref: int, - trading_pair: str, - amount: Decimal, - order_type: OrderType, - is_buy: bool, - price: Optional[Decimal] = s_decimal_NaN): + def generate_userref(self): + self._last_userref += 1 + return self._last_userref + + # todo 修改KrakenInFlightOrder + # def restore_tracking_states(self, saved_states: Dict[str, Any]): + # in_flight_orders: Dict[str, KrakenInFlightOrder] = {} + # for key, value in saved_states.items(): + # in_flight_orders[key] = KrakenInFlightOrder.from_json(value) + # self._last_userref = max(int(value["userref"]), self._last_userref) + # self._in_flight_orders.update(in_flight_orders) + # === 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 + ) + userref = self.generate_userref() + safe_ensure_future(self._create_order( + trade_type=TradeType.BUY, + order_id=order_id, + trading_pair=trading_pair, + amount=amount, + order_type=order_type, + price=price, + userref=userref)) + return 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 + ) + userref = self.generate_userref() + safe_ensure_future(self._create_order( + trade_type=TradeType.SELL, + order_id=order_id, + trading_pair=trading_pair, + amount=amount, + order_type=order_type, + price=price, + userref=userref)) + return order_id + async def _place_order(self, + order_id: str, + trading_pair: str, + amount: Decimal, + trade_type: TradeType, + order_type: OrderType, + price: Decimal, + **kwargs) -> Tuple[str, float]: + userref = kwargs.get("userref", 0) trading_pair = convert_to_exchange_trading_pair(trading_pair) data = { "pair": trading_pair, - "type": "buy" if is_buy else "sell", + "type": "buy" if trade_type is TradeType.BUY else "sell", "ordertype": "market" if order_type is OrderType.MARKET else "limit", "volume": str(amount), "userref": userref, @@ -210,9 +291,11 @@ async def place_order(self, } if order_type is OrderType.LIMIT_MAKER: data["oflags"] = "post" - order_result = await self._api_post(path_url=CONSTANTS.ADD_ORDER_PATH_URL, - data=data, - is_auth_required=True) + order_result = await self._api_request_with_retry("post", + CONSTANTS.ADD_ORDER_PATH_URL, + data=data, + is_auth_required=True) + # todo # o_order_result = order_result['response']["data"]["statuses"][0] @@ -222,60 +305,92 @@ async def place_order(self, o_id = order_result["txid"][0] return (o_id, self.current_timestamp) - async def _place_order(self, - order_id: str, - trading_pair: str, - amount: Decimal, - trade_type: TradeType, - order_type: OrderType, - price: Decimal, - **kwargs) -> Tuple[str, float]: - order_result = None - amount_str = f"{amount:f}" - type_str = KrakenExchange.kraken_order_type(order_type) - side_str = CONSTANTS.SIDE_BUY if trade_type is TradeType.BUY else CONSTANTS.SIDE_SELL - symbol = await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) - api_params = {"symbol": symbol, - "side": side_str, - "quantity": amount_str, - # "quoteOrderQty": amount_str, - "type": type_str, - "newClientOrderId": order_id} - if order_type.is_limit_type(): - price_str = f"{price:f}" - api_params["price"] = price_str - else: - if trade_type.name.lower() == 'buy': - if price.is_nan(): - price = self.get_price_for_volume( - trading_pair, - True, - amount + # todo + async def _api_request_with_retry(self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + is_auth_required: bool = False, + retry_interval = 2.0) -> Dict[str, Any]: + result = None + for retry_attempt in range(self.REQUEST_ATTEMPTS): + try: + result= await self._api_request(method, endpoint, params, data, is_auth_required) + break + except IOError as e: + if self.is_cloudflare_exception(e): + if endpoint == CONSTANTS.ADD_ORDER_PATH_URL: + self.logger().info(f"Retrying {endpoint}") + # Order placement could have been successful despite the IOError, so check for the open order. + response = self.get_open_orders_with_userref(data.get('userref')) + if any(response.get("open").values()): + return response + self.logger().warning( + f"Cloudflare error. Attempt {retry_attempt+1}/{self.REQUEST_ATTEMPTS}" + f" API command {method}: {endpoint}" ) - del api_params['quantity'] - api_params.update({ - "quoteOrderQty": f"{price * amount:f}", - }) - if order_type == OrderType.LIMIT: - api_params["timeInForce"] = CONSTANTS.TIME_IN_FORCE_GTC - - try: - order_result = await self._api_post( - path_url=CONSTANTS.ORDER_PATH_URL, - data=api_params, - is_auth_required=True) - o_id = str(order_result["orderId"]) - transact_time = order_result["transactTime"] * 1e-3 - except IOError as e: - error_description = str(e) - is_server_overloaded = ("status is 503" in error_description - and "Unknown error, please check your request or try again later." in error_description) - if is_server_overloaded: - o_id = "UNKNOWN" - transact_time = self._time_synchronizer.time() - else: + await asyncio.sleep(retry_interval ** retry_attempt) + continue + else: + raise e + if result is None: + raise IOError(f"Error fetching data from {endpoint}.") + return result + + # todo + async def _api_request(self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + is_auth_required: bool = False) -> Dict[str, Any]: + async with self._throttler.execute_task(endpoint): + url = f"{CONSTANTS.BASE_URL}{endpoint}" + headers = {} + data_dict = data if data is not None else {} + + if is_auth_required: + auth_dict: Dict[str, Any] = self._kraken_auth.generate_auth_dict(endpoint, data=data) + headers.update(auth_dict["headers"]) + data_dict = auth_dict["postDict"] + + request = RESTRequest( + method=RESTMethod[method.upper()], + url=url, + headers=headers, + params=params, + data=data_dict + ) + rest_assistant = await self._get_rest_assistant() + response = await rest_assistant.call(request=request, timeout=100) + + if response.status != 200: + raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.") + try: + response_json = await response.json() + except Exception: + raise IOError(f"Error parsing data from {url}.") + + try: + err = response_json["error"] + if "EOrder:Unknown order" in err or "EOrder:Invalid order" in err: + return {"error": err} + elif "EAPI:Invalid nonce" in err: + self.logger().error(f"Invalid nonce error from {url}. " + + "Please ensure your Kraken API key nonce window is at least 10, " + + "and if needed reset your API key.") + raise IOError({"error": response_json}) + except IOError: raise - return o_id, transact_time + except Exception: + pass + + data = response_json.get("result") + if data is None: + self.logger().error(f"Error received from {url}. Response is {response_json}.") + raise IOError({"error": response_json}) + return data async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): symbol = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py new file mode 100644 index 0000000000..1c73506534 --- /dev/null +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -0,0 +1,380 @@ +import asyncio +import copy +import math +import typing +from decimal import Decimal +from enum import Enum +from typing import Any, Dict, NamedTuple, Optional, Tuple + +from async_timeout import timeout + +from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.data_type.trade_fee import TradeFeeBase +from hummingbot.core.data_type.in_flight_order import InFlightOrder +if typing.TYPE_CHECKING: # avoid circular import problems + from hummingbot.connector.exchange_base import ExchangeBase + +s_decimal_0 = Decimal("0") + +GET_EX_ORDER_ID_TIMEOUT = 10 # seconds + +# todo +class OrderState(Enum): + PENDING_CREATE = 0 + OPEN = 1 + PENDING_CANCEL = 2 + CANCELED = 3 + PARTIALLY_FILLED = 4 + FILLED = 5 + FAILED = 6 + PENDING_APPROVAL = 7 + APPROVED = 8 + CREATED = 9 + COMPLETED = 10 + + +class OrderUpdate(NamedTuple): + trading_pair: str + update_timestamp: float # seconds + new_state: OrderState + client_order_id: Optional[str] = None + exchange_order_id: Optional[str] = None + misc_updates: Optional[Dict[str, Any]] = None + + +class TradeUpdate(NamedTuple): + trade_id: str + client_order_id: str + exchange_order_id: str + trading_pair: str + fill_timestamp: float # seconds + fill_price: Decimal + fill_base_amount: Decimal + fill_quote_amount: Decimal + fee: TradeFeeBase + is_taker: bool = True # CEXs deliver trade events from the taker's perspective + + @property + def fee_asset(self): + return self.fee.fee_asset + + @classmethod + def from_json(cls, data: Dict[str, Any]): + instance = TradeUpdate( + trade_id=data["trade_id"], + client_order_id=data["client_order_id"], + exchange_order_id=data["exchange_order_id"], + trading_pair=data["trading_pair"], + fill_timestamp=data["fill_timestamp"], + fill_price=Decimal(data["fill_price"]), + fill_base_amount=Decimal(data["fill_base_amount"]), + fill_quote_amount=Decimal(data["fill_quote_amount"]), + fee=TradeFeeBase.from_json(data["fee"]), + ) + + return instance + + def to_json(self) -> Dict[str, Any]: + json_dict = self._asdict() + json_dict.update({ + "fill_price": str(self.fill_price), + "fill_base_amount": str(self.fill_base_amount), + "fill_quote_amount": str(self.fill_quote_amount), + "fee": self.fee.to_json(), + }) + return json_dict + + +class KrakenInFlightOrder(InFlightOrder): + def __init__( + self, + client_order_id: str, + trading_pair: str, + order_type: OrderType, + trade_type: TradeType, + amount: Decimal, + creation_timestamp: float, + price: Optional[Decimal] = None, + exchange_order_id: Optional[str] = None, + initial_state: OrderState = OrderState.PENDING_CREATE, + leverage: int = 1, + position: PositionAction = PositionAction.NIL, + ) -> None: + self.client_order_id = client_order_id + self.creation_timestamp = creation_timestamp + self.trading_pair = trading_pair + self.order_type = order_type + self.trade_type = trade_type + self.price = price + self.amount = amount + self.exchange_order_id = exchange_order_id + self.current_state = initial_state + self.leverage = leverage + self.position = position + + self.executed_amount_base = s_decimal_0 + self.executed_amount_quote = s_decimal_0 + + self.last_update_timestamp: float = creation_timestamp + + self.order_fills: Dict[str, TradeUpdate] = {} # Dict[trade_id, TradeUpdate] + + self.exchange_order_id_update_event = asyncio.Event() + if self.exchange_order_id: + self.exchange_order_id_update_event.set() + self.completely_filled_event = asyncio.Event() + self.processed_by_exchange_event = asyncio.Event() + self.check_processed_by_exchange_condition() + + @property + def attributes(self) -> Tuple[Any]: + return copy.deepcopy( + ( + self.client_order_id, + self.trading_pair, + self.order_type, + self.trade_type, + self.price, + self.amount, + self.exchange_order_id, + self.current_state, + self.leverage, + self.position, + self.executed_amount_base, + self.executed_amount_quote, + self.creation_timestamp, + self.last_update_timestamp, + ) + ) + + def __eq__(self, other: object) -> bool: + return type(self) is type(other) and self.attributes == other.attributes + + @property + def base_asset(self): + return self.trading_pair.split("-")[0] + + @property + def quote_asset(self): + return self.trading_pair.split("-")[1] + + @property + def is_pending_create(self) -> bool: + return self.current_state == OrderState.PENDING_CREATE + + @property + def is_pending_cancel_confirmation(self) -> bool: + return self.current_state == OrderState.PENDING_CANCEL + + @property + def is_open(self) -> bool: + return self.current_state in { + OrderState.PENDING_CREATE, + OrderState.OPEN, + OrderState.PARTIALLY_FILLED, + OrderState.PENDING_CANCEL} + + @property + def is_done(self) -> bool: + return ( + self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} + or math.isclose(self.executed_amount_base, self.amount) + or self.executed_amount_base >= self.amount + ) + + @property + def is_filled(self) -> bool: + return ( + self.current_state == OrderState.FILLED + or (self.amount != s_decimal_0 + and (math.isclose(self.executed_amount_base, self.amount) + or self.executed_amount_base >= self.amount) + ) + ) + + @property + def is_failure(self) -> bool: + return self.current_state == OrderState.FAILED + + @property + def is_cancelled(self) -> bool: + return self.current_state == OrderState.CANCELED + + @property + def average_executed_price(self) -> Optional[Decimal]: + executed_value: Decimal = s_decimal_0 + total_base_amount: Decimal = s_decimal_0 + for order_fill in self.order_fills.values(): + executed_value += order_fill.fill_price * order_fill.fill_base_amount + total_base_amount += order_fill.fill_base_amount + if executed_value == s_decimal_0 or total_base_amount == s_decimal_0: + return None + return executed_value / total_base_amount + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": + """ + Initialize an InFlightOrder using a JSON object + :param data: JSON data + :return: Formatted InFlightOrder + """ + order = InFlightOrder( + client_order_id=data["client_order_id"], + trading_pair=data["trading_pair"], + order_type=getattr(OrderType, data["order_type"]), + trade_type=getattr(TradeType, data["trade_type"]), + amount=Decimal(data["amount"]), + price=Decimal(data["price"]), + exchange_order_id=data["exchange_order_id"], + initial_state=OrderState(int(data["last_state"])), + leverage=int(data["leverage"]), + position=PositionAction(data["position"]), + creation_timestamp=data.get("creation_timestamp", -1) + ) + order.executed_amount_base = Decimal(data["executed_amount_base"]) + order.executed_amount_quote = Decimal(data["executed_amount_quote"]) + order.order_fills.update({key: TradeUpdate.from_json(value) + for key, value + in data.get("order_fills", {}).items()}) + order.last_update_timestamp = data.get("last_update_timestamp", order.creation_timestamp) + + order.check_filled_condition() + order.check_processed_by_exchange_condition() + + return order + + def to_json(self) -> Dict[str, Any]: + """ + Returns this InFlightOrder as a JSON object. + :return: JSON object + """ + return { + "client_order_id": self.client_order_id, + "exchange_order_id": self.exchange_order_id, + "trading_pair": self.trading_pair, + "order_type": self.order_type.name, + "trade_type": self.trade_type.name, + "price": str(self.price), + "amount": str(self.amount), + "executed_amount_base": str(self.executed_amount_base), + "executed_amount_quote": str(self.executed_amount_quote), + "last_state": str(self.current_state.value), + "leverage": str(self.leverage), + "position": self.position.value, + "creation_timestamp": self.creation_timestamp, + "last_update_timestamp": self.last_update_timestamp, + "order_fills": {key: fill.to_json() for key, fill in self.order_fills.items()} + } + + def to_limit_order(self) -> LimitOrder: + """ + Returns this InFlightOrder as a LimitOrder object. + :return: LimitOrder object. + """ + return LimitOrder( + client_order_id=self.client_order_id, + trading_pair=self.trading_pair, + is_buy=self.trade_type is TradeType.BUY, + base_currency=self.base_asset, + quote_currency=self.quote_asset, + price=self.price, + quantity=self.amount, + filled_quantity=self.executed_amount_base, + creation_timestamp=int(self.creation_timestamp * 1e6) + ) + + def update_exchange_order_id(self, exchange_order_id: str): + self.exchange_order_id = exchange_order_id + self.exchange_order_id_update_event.set() + + async def get_exchange_order_id(self): + if self.exchange_order_id is None: + async with timeout(GET_EX_ORDER_ID_TIMEOUT): + await self.exchange_order_id_update_event.wait() + return self.exchange_order_id + + def cumulative_fee_paid(self, token: str, exchange: Optional['ExchangeBase'] = None) -> Decimal: + """ + Returns the total amount of fee paid for each traid update, expressed in the specified token + :param token: The token all partial fills' fees should be transformed to before summing them + :param exchange: The exchange being used. If specified the logic will try to use the order book to get the rate + :return: the cumulative fee paid for all partial fills in the specified token + """ + total_fee_in_token = Decimal("0") + for trade_update in self.order_fills.values(): + total_fee_in_token += trade_update.fee.fee_amount_in_token( + trading_pair=trade_update.trading_pair, + price=trade_update.fill_price, + order_amount=trade_update.fill_base_amount, + token=token, + exchange=exchange + ) + + return total_fee_in_token + + def update_with_order_update(self, order_update: OrderUpdate) -> bool: + """ + Updates the in flight order with an order update (from REST API or WS API) + return: True if the order gets updated otherwise False + """ + if (order_update.client_order_id != self.client_order_id + and order_update.exchange_order_id != self.exchange_order_id): + return False + + prev_data = (self.exchange_order_id, self.current_state) + + if self.exchange_order_id is None and order_update.exchange_order_id is not None: + self.update_exchange_order_id(order_update.exchange_order_id) + + self.current_state = order_update.new_state + self.check_processed_by_exchange_condition() + + updated: bool = prev_data != (self.exchange_order_id, self.current_state) + + if updated: + self.last_update_timestamp = order_update.update_timestamp + + return updated + + def update_with_trade_update(self, trade_update: TradeUpdate) -> bool: + """ + Updates the in flight order with a trade update (from REST API or WS API) + :return: True if the order gets updated otherwise False + """ + trade_id: str = trade_update.trade_id + + if (trade_id in self.order_fills + or (self.client_order_id != trade_update.client_order_id + and self.exchange_order_id != trade_update.exchange_order_id)): + return False + + self.order_fills[trade_id] = trade_update + + self.executed_amount_base += trade_update.fill_base_amount + self.executed_amount_quote += trade_update.fill_quote_amount + + self.last_update_timestamp = trade_update.fill_timestamp + self.check_filled_condition() + + return True + + def check_filled_condition(self): + if (abs(self.amount) - self.executed_amount_base).quantize(Decimal('1e-8')) <= 0: + self.completely_filled_event.set() + + async def wait_until_completely_filled(self): + await self.completely_filled_event.wait() + + def check_processed_by_exchange_condition(self): + if self.current_state.value > OrderState.PENDING_CREATE.value: + self.processed_by_exchange_event.set() + + async def wait_until_processed_by_exchange(self): + await self.processed_by_exchange_event.wait() + + def build_order_created_message(self) -> str: + return ( + f"Created {self.order_type.name.upper()} {self.trade_type.name.upper()} order " + f"{self.client_order_id} for {self.amount} {self.trading_pair}." + ) From cf78b963d1c3953b670d154554bffd4bede9a87f Mon Sep 17 00:00:00 2001 From: bczhang Date: Fri, 26 Jan 2024 21:43:03 +0800 Subject: [PATCH 05/34] add kraken_in_fight_order --- .../exchange/kraken/kraken_in_fight_order.py | 226 ++---------------- .../kraken_v1/kraken_in_flight_order.pyx | 1 + 2 files changed, 25 insertions(+), 202 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index 1c73506534..0d582342de 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -95,37 +95,28 @@ def __init__( trade_type: TradeType, amount: Decimal, creation_timestamp: float, + userref: int, price: Optional[Decimal] = None, exchange_order_id: Optional[str] = None, initial_state: OrderState = OrderState.PENDING_CREATE, leverage: int = 1, position: PositionAction = PositionAction.NIL, - ) -> None: - self.client_order_id = client_order_id - self.creation_timestamp = creation_timestamp - self.trading_pair = trading_pair - self.order_type = order_type - self.trade_type = trade_type - self.price = price - self.amount = amount - self.exchange_order_id = exchange_order_id - self.current_state = initial_state - self.leverage = leverage - self.position = position - - self.executed_amount_base = s_decimal_0 - self.executed_amount_quote = s_decimal_0 - - self.last_update_timestamp: float = creation_timestamp - - self.order_fills: Dict[str, TradeUpdate] = {} # Dict[trade_id, TradeUpdate] - self.exchange_order_id_update_event = asyncio.Event() - if self.exchange_order_id: - self.exchange_order_id_update_event.set() - self.completely_filled_event = asyncio.Event() - self.processed_by_exchange_event = asyncio.Event() - self.check_processed_by_exchange_condition() + ) -> None: + super().__init__( + client_order_id, + trading_pair, + order_type, + trade_type, + amount, + creation_timestamp, + price, + exchange_order_id, + initial_state, + leverage, + position, + ) + self.userref = userref @property def attributes(self) -> Tuple[Any]: @@ -141,76 +132,17 @@ def attributes(self) -> Tuple[Any]: self.current_state, self.leverage, self.position, + self.userref, self.executed_amount_base, self.executed_amount_quote, self.creation_timestamp, self.last_update_timestamp, ) ) - - def __eq__(self, other: object) -> bool: - return type(self) is type(other) and self.attributes == other.attributes - - @property - def base_asset(self): - return self.trading_pair.split("-")[0] - - @property - def quote_asset(self): - return self.trading_pair.split("-")[1] - - @property - def is_pending_create(self) -> bool: - return self.current_state == OrderState.PENDING_CREATE - - @property - def is_pending_cancel_confirmation(self) -> bool: - return self.current_state == OrderState.PENDING_CANCEL - + # todo @property - def is_open(self) -> bool: - return self.current_state in { - OrderState.PENDING_CREATE, - OrderState.OPEN, - OrderState.PARTIALLY_FILLED, - OrderState.PENDING_CANCEL} - - @property - def is_done(self) -> bool: - return ( - self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} - or math.isclose(self.executed_amount_base, self.amount) - or self.executed_amount_base >= self.amount - ) - - @property - def is_filled(self) -> bool: - return ( - self.current_state == OrderState.FILLED - or (self.amount != s_decimal_0 - and (math.isclose(self.executed_amount_base, self.amount) - or self.executed_amount_base >= self.amount) - ) - ) - - @property - def is_failure(self) -> bool: - return self.current_state == OrderState.FAILED - - @property - def is_cancelled(self) -> bool: - return self.current_state == OrderState.CANCELED - - @property - def average_executed_price(self) -> Optional[Decimal]: - executed_value: Decimal = s_decimal_0 - total_base_amount: Decimal = s_decimal_0 - for order_fill in self.order_fills.values(): - executed_value += order_fill.fill_price * order_fill.fill_base_amount - total_base_amount += order_fill.fill_base_amount - if executed_value == s_decimal_0 or total_base_amount == s_decimal_0: - return None - return executed_value / total_base_amount + def is_local(self) -> bool: + return self.last_state in {"local"} @classmethod def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": @@ -219,7 +151,7 @@ def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": :param data: JSON data :return: Formatted InFlightOrder """ - order = InFlightOrder( + order = KrakenInFlightOrder( client_order_id=data["client_order_id"], trading_pair=data["trading_pair"], order_type=getattr(OrderType, data["order_type"]), @@ -230,7 +162,8 @@ def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": initial_state=OrderState(int(data["last_state"])), leverage=int(data["leverage"]), position=PositionAction(data["position"]), - creation_timestamp=data.get("creation_timestamp", -1) + creation_timestamp=data.get("creation_timestamp", -1), + userref=data.get("userref", 0) ) order.executed_amount_base = Decimal(data["executed_amount_base"]) order.executed_amount_quote = Decimal(data["executed_amount_quote"]) @@ -262,119 +195,8 @@ def to_json(self) -> Dict[str, Any]: "last_state": str(self.current_state.value), "leverage": str(self.leverage), "position": self.position.value, + "userref": self.userref, "creation_timestamp": self.creation_timestamp, "last_update_timestamp": self.last_update_timestamp, "order_fills": {key: fill.to_json() for key, fill in self.order_fills.items()} } - - def to_limit_order(self) -> LimitOrder: - """ - Returns this InFlightOrder as a LimitOrder object. - :return: LimitOrder object. - """ - return LimitOrder( - client_order_id=self.client_order_id, - trading_pair=self.trading_pair, - is_buy=self.trade_type is TradeType.BUY, - base_currency=self.base_asset, - quote_currency=self.quote_asset, - price=self.price, - quantity=self.amount, - filled_quantity=self.executed_amount_base, - creation_timestamp=int(self.creation_timestamp * 1e6) - ) - - def update_exchange_order_id(self, exchange_order_id: str): - self.exchange_order_id = exchange_order_id - self.exchange_order_id_update_event.set() - - async def get_exchange_order_id(self): - if self.exchange_order_id is None: - async with timeout(GET_EX_ORDER_ID_TIMEOUT): - await self.exchange_order_id_update_event.wait() - return self.exchange_order_id - - def cumulative_fee_paid(self, token: str, exchange: Optional['ExchangeBase'] = None) -> Decimal: - """ - Returns the total amount of fee paid for each traid update, expressed in the specified token - :param token: The token all partial fills' fees should be transformed to before summing them - :param exchange: The exchange being used. If specified the logic will try to use the order book to get the rate - :return: the cumulative fee paid for all partial fills in the specified token - """ - total_fee_in_token = Decimal("0") - for trade_update in self.order_fills.values(): - total_fee_in_token += trade_update.fee.fee_amount_in_token( - trading_pair=trade_update.trading_pair, - price=trade_update.fill_price, - order_amount=trade_update.fill_base_amount, - token=token, - exchange=exchange - ) - - return total_fee_in_token - - def update_with_order_update(self, order_update: OrderUpdate) -> bool: - """ - Updates the in flight order with an order update (from REST API or WS API) - return: True if the order gets updated otherwise False - """ - if (order_update.client_order_id != self.client_order_id - and order_update.exchange_order_id != self.exchange_order_id): - return False - - prev_data = (self.exchange_order_id, self.current_state) - - if self.exchange_order_id is None and order_update.exchange_order_id is not None: - self.update_exchange_order_id(order_update.exchange_order_id) - - self.current_state = order_update.new_state - self.check_processed_by_exchange_condition() - - updated: bool = prev_data != (self.exchange_order_id, self.current_state) - - if updated: - self.last_update_timestamp = order_update.update_timestamp - - return updated - - def update_with_trade_update(self, trade_update: TradeUpdate) -> bool: - """ - Updates the in flight order with a trade update (from REST API or WS API) - :return: True if the order gets updated otherwise False - """ - trade_id: str = trade_update.trade_id - - if (trade_id in self.order_fills - or (self.client_order_id != trade_update.client_order_id - and self.exchange_order_id != trade_update.exchange_order_id)): - return False - - self.order_fills[trade_id] = trade_update - - self.executed_amount_base += trade_update.fill_base_amount - self.executed_amount_quote += trade_update.fill_quote_amount - - self.last_update_timestamp = trade_update.fill_timestamp - self.check_filled_condition() - - return True - - def check_filled_condition(self): - if (abs(self.amount) - self.executed_amount_base).quantize(Decimal('1e-8')) <= 0: - self.completely_filled_event.set() - - async def wait_until_completely_filled(self): - await self.completely_filled_event.wait() - - def check_processed_by_exchange_condition(self): - if self.current_state.value > OrderState.PENDING_CREATE.value: - self.processed_by_exchange_event.set() - - async def wait_until_processed_by_exchange(self): - await self.processed_by_exchange_event.wait() - - def build_order_created_message(self) -> str: - return ( - f"Created {self.order_type.name.upper()} {self.trade_type.name.upper()} order " - f"{self.client_order_id} for {self.amount} {self.trading_pair}." - ) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx index 111e9bd125..2e2362557b 100644 --- a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx +++ b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx @@ -42,6 +42,7 @@ cdef class KrakenInFlightOrder(InFlightOrderBase): self.trade_id_set = set() self.userref = userref + # todo @property def is_local(self) -> bool: return self.last_state in {"local"} From 385be07ce9364bd822e5513e0621625f6c43af57 Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 1 Feb 2024 23:15:37 +0800 Subject: [PATCH 06/34] update balance& trading rule --- .../exchange/kraken/kraken_exchange.py | 266 ++++++++++++------ .../exchange/kraken/kraken_web_utils.py | 36 +-- 2 files changed, 193 insertions(+), 109 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index e90fce2701..22ac510eb0 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -1,6 +1,8 @@ import asyncio +from collections import defaultdict from decimal import Decimal from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +import re from bidict import bidict @@ -20,6 +22,10 @@ from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.connector.exchange.kraken.kraken_in_fight_order import ( + KrakenInFlightOrder, + # KrakenInFlightOrderNotCreated, +) from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule from hummingbot.connector.utils import TradeFillOrderDetails, combine_to_hb_trading_pair, get_new_client_order_id @@ -44,6 +50,7 @@ class KrakenExchange(ExchangePyBase): SHORT_POLL_INTERVAL = 30.0 web_utils = web_utils + REQUEST_ATTEMPTS = 5 def __init__(self, client_config_map: "ClientConfigAdapter", @@ -63,6 +70,7 @@ def __init__(self, self._last_trades_poll_kraken_timestamp = 1.0 self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) + self._asset_pairs = {} self._last_userref = 0 super().__init__(client_config_map) @@ -198,6 +206,20 @@ def generate_userref(self): self._last_userref += 1 return self._last_userref + @staticmethod + def is_cloudflare_exception(exception: Exception): + """ + Error status 5xx or 10xx are related to Cloudflare. + https://support.kraken.com/hc/en-us/articles/360001491786-API-error-messages#6 + """ + return bool(re.search(r"HTTP status is (5|10)\d\d\.", str(exception))) + + async def get_open_orders_with_userref(self, userref: int): + data = {'userref': userref} + return await self._api_request_with_retry(RESTMethod.POST, + CONSTANTS.OPEN_ORDERS_PATH_URL, + is_auth_required=True, + data=data) # todo 修改KrakenInFlightOrder # def restore_tracking_states(self, saved_states: Dict[str, Any]): # in_flight_orders: Dict[str, KrakenInFlightOrder] = {} @@ -229,7 +251,7 @@ def buy(self, hbot_order_id_prefix=self.client_order_id_prefix, max_id_len=self.client_order_id_max_length ) - userref = self.generate_userref() + userref = self.generate_userref() safe_ensure_future(self._create_order( trade_type=TradeType.BUY, order_id=order_id, @@ -260,7 +282,7 @@ def sell(self, hbot_order_id_prefix=self.client_order_id_prefix, max_id_len=self.client_order_id_max_length ) - userref = self.generate_userref() + userref = self.generate_userref() safe_ensure_future(self._create_order( trade_type=TradeType.SELL, order_id=order_id, @@ -271,6 +293,58 @@ def sell(self, userref=userref)) return order_id + async def get_asset_pairs(self) -> Dict[str, Any]: + if not self._asset_pairs: + asset_pairs = await self._api_request(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) + self._asset_pairs = {f"{details['base']}-{details['quote']}": details + for _, details in asset_pairs.items() if not is_dark_pool(details)} + return self._asset_pairs + + def start_tracking_order(self, + order_id: str, + exchange_order_id: Optional[str], + trading_pair: str, + trade_type: TradeType, + price: Decimal, + amount: Decimal, + order_type: OrderType, + **kwargs): + """ + Starts tracking an order by adding it to the order tracker. + + :param order_id: the order identifier + :param exchange_order_id: the identifier for the order in the exchange + :param trading_pair: the token pair for the operation + :param trade_type: the type of order (buy or sell) + :param price: the price for the order + :param amount: the amount for the order + :param order_type: type of execution for the order (MARKET, LIMIT, LIMIT_MAKER) + """ + userref = kwargs.get("userref", 0) + self._order_tracker.start_tracking_order( + KrakenInFlightOrder( + client_order_id=order_id, + exchange_order_id=exchange_order_id, + trading_pair=trading_pair, + order_type=order_type, + trade_type=trade_type, + amount=amount, + price=price, + creation_timestamp=self.current_timestamp, + userref=userref, + ) + ) + + def restore_tracking_states(self, saved_states: Dict[str, Any]): + for serialized_order in saved_states.values(): + order = KrakenInFlightOrder.from_json(serialized_order) + if order.is_open: + self._order_tracker._in_flight_orders[order.client_order_id] = order + elif order.is_failure: + # If the order is marked as failed but is still in the tracking states, it was a lost order + self._order_tracker._lost_orders[order.client_order_id] = order + self._last_userref = max(int(serialized_order.userref), self._last_userref) + async def _place_order(self, order_id: str, trading_pair: str, @@ -291,11 +365,10 @@ async def _place_order(self, } if order_type is OrderType.LIMIT_MAKER: data["oflags"] = "post" - order_result = await self._api_request_with_retry("post", - CONSTANTS.ADD_ORDER_PATH_URL, - data=data, - is_auth_required=True) - + order_result = await self._api_request_with_retry(RESTMethod.POST, + CONSTANTS.ADD_ORDER_PATH_URL, + data=data, + is_auth_required=True) # todo # o_order_result = order_result['response']["data"]["statuses"][0] @@ -307,16 +380,17 @@ async def _place_order(self, # todo async def _api_request_with_retry(self, - method: str, + method: RESTMethod, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, is_auth_required: bool = False, - retry_interval = 2.0) -> Dict[str, Any]: + retry_interval=2.0) -> Dict[str, Any]: result = None for retry_attempt in range(self.REQUEST_ATTEMPTS): try: - result= await self._api_request(method, endpoint, params, data, is_auth_required) + result = await self._api_request(path_url=endpoint, method=method, params=params, data=data, + is_auth_required=is_auth_required) break except IOError as e: if self.is_cloudflare_exception(e): @@ -327,7 +401,7 @@ async def _api_request_with_retry(self, if any(response.get("open").values()): return response self.logger().warning( - f"Cloudflare error. Attempt {retry_attempt+1}/{self.REQUEST_ATTEMPTS}" + f"Cloudflare error. Attempt {retry_attempt + 1}/{self.REQUEST_ATTEMPTS}" f" API command {method}: {endpoint}" ) await asyncio.sleep(retry_interval ** retry_attempt) @@ -338,60 +412,6 @@ async def _api_request_with_retry(self, raise IOError(f"Error fetching data from {endpoint}.") return result - # todo - async def _api_request(self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - is_auth_required: bool = False) -> Dict[str, Any]: - async with self._throttler.execute_task(endpoint): - url = f"{CONSTANTS.BASE_URL}{endpoint}" - headers = {} - data_dict = data if data is not None else {} - - if is_auth_required: - auth_dict: Dict[str, Any] = self._kraken_auth.generate_auth_dict(endpoint, data=data) - headers.update(auth_dict["headers"]) - data_dict = auth_dict["postDict"] - - request = RESTRequest( - method=RESTMethod[method.upper()], - url=url, - headers=headers, - params=params, - data=data_dict - ) - rest_assistant = await self._get_rest_assistant() - response = await rest_assistant.call(request=request, timeout=100) - - if response.status != 200: - raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.") - try: - response_json = await response.json() - except Exception: - raise IOError(f"Error parsing data from {url}.") - - try: - err = response_json["error"] - if "EOrder:Unknown order" in err or "EOrder:Invalid order" in err: - return {"error": err} - elif "EAPI:Invalid nonce" in err: - self.logger().error(f"Invalid nonce error from {url}. " + - "Please ensure your Kraken API key nonce window is at least 10, " + - "and if needed reset your API key.") - raise IOError({"error": response_json}) - except IOError: - raise - except Exception: - pass - - data = response_json.get("result") - if data is None: - self.logger().error(f"Error received from {url}. Response is {response_json}.") - raise IOError({"error": response_json}) - return data - async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): symbol = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) api_params = { @@ -407,24 +427,70 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): return False async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> List[TradingRule]: - trading_pair_rules = exchange_info_dict.get("symbols", []) - retval = [] - for rule in filter(kraken_utils.is_exchange_information_valid, trading_pair_rules): + """ + Example: + { + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } + } + """ + retval: list = [] + trading_pair_rules = exchange_info_dict.values() + # for trading_pair, rule in asset_pairs_dict.items(): + for rule in filter(web_utils.is_exchange_information_valid, trading_pair_rules): try: trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol=rule.get("symbol")) - min_order_size = Decimal(rule.get("baseSizePrecision")) - min_price_inc = Decimal(f"1e-{rule['quotePrecision']}") - min_amount_inc = Decimal(f"1e-{rule['baseAssetPrecision']}") - min_notional = Decimal(rule['quoteAmountPrecision']) + min_order_size = Decimal(rule.get('ordermin', 0)) + min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") + min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") retval.append( - TradingRule(trading_pair, - min_order_size=min_order_size, - min_price_increment=min_price_inc, - min_base_amount_increment=min_amount_inc, - min_notional_size=min_notional)) - + TradingRule( + trading_pair, + min_order_size=min_order_size, + min_price_increment=min_price_increment, + min_base_amount_increment=min_base_amount_increment, + ) + ) except Exception: - self.logger().exception(f"Error parsing the trading pair rule {rule}. Skipping.") + self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True) return retval async def _status_polling_loop_fetch_updates(self): @@ -693,30 +759,44 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda async def _update_balances(self): local_asset_names = set(self._account_balances.keys()) remote_asset_names = set() + balances = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.BALANCE_PATH_URL, is_auth_required=True) + open_orders = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.OPEN_ORDERS_PATH_URL, is_auth_required=True) - account_info = await self._api_get( - path_url=CONSTANTS.ACCOUNTS_PATH_URL, - is_auth_required=True) + locked = defaultdict(Decimal) - balances = account_info["balances"] - for balance_entry in balances: - asset_name = balance_entry["asset"] - free_balance = Decimal(balance_entry["free"]) - total_balance = Decimal(balance_entry["free"]) + Decimal(balance_entry["locked"]) - self._account_available_balances[asset_name] = free_balance - self._account_balances[asset_name] = total_balance - remote_asset_names.add(asset_name) + for order in open_orders.get("open").values(): + if order.get("status") == "open": + details = order.get("descr") + if details.get("ordertype") == "limit": + pair = convert_from_exchange_trading_pair( + details.get("pair"), tuple((await self.get_asset_pairs()).keys()) + ) + (base, quote) = self.split_trading_pair(pair) + vol_locked = Decimal(order.get("vol", 0)) - Decimal(order.get("vol_exec", 0)) + if details.get("type") == "sell": + locked[convert_from_exchange_symbol(base)] += vol_locked + elif details.get("type") == "buy": + locked[convert_from_exchange_symbol(quote)] += vol_locked * Decimal(details.get("price")) + + for asset_name, balance in balances.items(): + cleaned_name = convert_from_exchange_symbol(asset_name).upper() + total_balance = Decimal(balance) + free_balance = total_balance - Decimal(locked[cleaned_name]) + self._account_available_balances[cleaned_name] = free_balance + self._account_balances[cleaned_name] = total_balance + remote_asset_names.add(cleaned_name) asset_names_to_remove = local_asset_names.difference(remote_asset_names) for asset_name in asset_names_to_remove: del self._account_available_balances[asset_name] del self._account_balances[asset_name] + def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]): mapping = bidict() - for symbol_data in filter(kraken_utils.is_exchange_information_valid, exchange_info["symbols"]): - mapping[symbol_data["symbol"]] = combine_to_hb_trading_pair(base=symbol_data["baseAsset"], - quote=symbol_data["quoteAsset"]) + for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info.values()): + mapping[symbol_data["altname"]] = combine_to_hb_trading_pair(base=symbol_data["base"], + quote=symbol_data["quote"]) self._set_trading_pair_symbol_map(mapping) async def _get_last_traded_price(self, trading_pair: str) -> float: diff --git a/hummingbot/connector/exchange/kraken/kraken_web_utils.py b/hummingbot/connector/exchange/kraken/kraken_web_utils.py index 0a48f7216e..9ad5ad8781 100644 --- a/hummingbot/connector/exchange/kraken/kraken_web_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_web_utils.py @@ -9,25 +9,18 @@ from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory -def public_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> str: - """ - Creates a full URL for provided public REST endpoint - :param path_url: a public REST endpoint - :param domain: the Kraken domain to connect to ("com" or "us"). The default value is "com" - :return: the full URL to the endpoint - """ - return CONSTANTS.REST_URL.format(domain) + CONSTANTS.PUBLIC_API_VERSION + path_url +def private_rest_url(*args, **kwargs) -> str: + return rest_url(*args, **kwargs) -def private_rest_url(path_url: str, domain: str = CONSTANTS.DEFAULT_DOMAIN) -> str: - """ - Creates a full URL for provided private REST endpoint - :param path_url: a private REST endpoint - :param domain: the Kraken domain to connect to ("com" or "us"). The default value is "com" - :return: the full URL to the endpoint - """ - return CONSTANTS.REST_URL.format(domain) + CONSTANTS.PRIVATE_API_VERSION + path_url +def public_rest_url(*args, **kwargs) -> str: + return rest_url(*args, **kwargs) + +def rest_url(path_url: str, domain: str = "kraken"): + # base_url = CONSTANTS.BASE_URL if domain == "kraken" else CONSTANTS.TESTNET_BASE_URL + base_url = CONSTANTS.BASE_URL + return base_url + path_url def build_api_factory( throttler: Optional[AsyncThrottler] = None, @@ -73,3 +66,14 @@ async def get_current_server_time( ) server_time = response["serverTime"] return server_time + + +def is_exchange_information_valid(rule) -> bool: + """ + Verifies if a trading pair is enabled to operate with based on its exchange information + + :param exchange_info: the exchange information for a trading pair + + :return: True if the trading pair is enabled, False otherwise + """ + return True \ No newline at end of file From 90548a422d6e582d306b5fc327e622c5dbbab104 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 6 Feb 2024 01:07:06 +0800 Subject: [PATCH 07/34] add trades & orders --- .../exchange/kraken/kraken_constants.py | 19 ++ .../exchange/kraken/kraken_exchange.py | 211 ++++-------------- .../exchange/kraken/kraken_in_fight_order.py | 4 - 3 files changed, 65 insertions(+), 169 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index d19e5ee74a..38aee3e2b7 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -4,6 +4,7 @@ Tuple, ) from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair +from hummingbot.core.data_type.in_flight_order import OrderState DEFAULT_DOMAIN="kraken" MAX_ORDER_ID_LEN = 32 @@ -51,6 +52,24 @@ class KrakenAPITier(Enum): BALANCE_PATH_URL = "/0/private/Balance" OPEN_ORDERS_PATH_URL = "/0/private/OpenOrders" QUERY_ORDERS_PATH_URL = "/0/private/QueryOrders" +QUERY_TRADES_PATH_URL = "/0/private/QueryTrades" + + +# Order States +ORDER_STATE = { + "pending": OrderState.PENDING_CREATE, + "open": OrderState.OPEN, + "closed": OrderState.COMPLETED, + "canceled": OrderState.CANCELED, + "expired": OrderState.FAILED, + + "FILLED": OrderState.FILLED, + "PARTIALLY_FILLED": OrderState.PARTIALLY_FILLED, + "PENDING_CANCEL": OrderState.OPEN, + "PARTIALLY_CANCELED": OrderState.CANCELED, + "REJECTED": OrderState.FAILED, + "EXPIRED": OrderState.FAILED, +} WS_URL = "wss://ws.kraken.com" WS_AUTH_URL = "wss://ws-auth.kraken.com/" diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 22ac510eb0..a90c09d00d 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -220,13 +220,7 @@ async def get_open_orders_with_userref(self, userref: int): CONSTANTS.OPEN_ORDERS_PATH_URL, is_auth_required=True, data=data) - # todo 修改KrakenInFlightOrder - # def restore_tracking_states(self, saved_states: Dict[str, Any]): - # in_flight_orders: Dict[str, KrakenInFlightOrder] = {} - # for key, value in saved_states.items(): - # in_flight_orders[key] = KrakenInFlightOrder.from_json(value) - # self._last_userref = max(int(value["userref"]), self._last_userref) - # self._in_flight_orders.update(in_flight_orders) + # === Orders placing === def buy(self, @@ -413,16 +407,16 @@ async def _api_request_with_retry(self, return result async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): - symbol = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) api_params = { - "symbol": symbol, - "origClientOrderId": order_id, + "txid": tracked_order.exchange_order_id, } - cancel_result = await self._api_delete( - path_url=CONSTANTS.ORDER_PATH_URL, - params=api_params, + cancel_result = await self._api_request_with_retry( + method=RESTMethod.POST, + endpoint=CONSTANTS.CANCEL_ORDER_PATH_URL, + data=api_params, is_auth_required=True) - if cancel_result.get("status") == "NEW": + if isinstance(cancel_result, dict) and ( + cancel_result.get("count") == 1 or cancel_result.get("error") is not None): return True return False @@ -493,9 +487,6 @@ async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> Lis self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True) return retval - async def _status_polling_loop_fetch_updates(self): - await self._update_order_fills_from_trades() - await super()._status_polling_loop_fetch_updates() async def _update_trading_fees(self): """ @@ -544,26 +535,27 @@ def _create_trade_update_with_order_fill_data( self, order_fill: Dict[str, Any], order: InFlightOrder): + fee_asset = order.quote_asset fee = TradeFeeBase.new_spot_fee( fee_schema=self.trade_fee_schema(), trade_type=order.trade_type, - percent_token=order_fill["N"], + percent_token=fee_asset, flat_fees=[TokenAmount( - amount=Decimal(order_fill["n"]), - token=order_fill["N"] + amount=Decimal(order_fill["fee"]), + token=fee_asset )] ) trade_update = TradeUpdate( - trade_id=str(order_fill["t"]), + trade_id=str(order_fill["trade_id"]), client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=order.trading_pair, fee=fee, - fill_base_amount=Decimal(order_fill["v"]), - fill_quote_amount=Decimal(order_fill["a"]), - fill_price=Decimal(order_fill["p"]), - fill_timestamp=order_fill["T"] * 1e-3, + fill_base_amount=Decimal(order_fill["vol"]), + fill_quote_amount=Decimal(order_fill["vol"]) * Decimal(order_fill["price"]), + fill_price=Decimal(order_fill["price"]), + fill_timestamp=order_fill["time"], ) return trade_update @@ -600,157 +592,46 @@ def _process_order_message(self, raw_msg: Dict[str, Any]): order_update = self._create_order_update_with_order_status_data(order_status=raw_msg, order=tracked_order) self._order_tracker.process_order_update(order_update=order_update) - async def _update_order_fills_from_trades(self): - """ - This is intended to be a backup measure to get filled events with trade ID for orders, - in case Kraken's user stream events are not working. - NOTE: It is not required to copy this functionality in other connectors. - This is separated from _update_order_status which only updates the order status without producing filled - events, since Kraken's get order endpoint does not return trade IDs. - The minimum poll interval for order status is 10 seconds. - """ - small_interval_last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL - small_interval_current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL - long_interval_last_tick = self._last_poll_timestamp / self.LONG_POLL_INTERVAL - long_interval_current_tick = self.current_timestamp / self.LONG_POLL_INTERVAL - - if (long_interval_current_tick > long_interval_last_tick - or (self.in_flight_orders and small_interval_current_tick > small_interval_last_tick)): - query_time = int(self._last_trades_poll_kraken_timestamp * 1e3) - self._last_trades_poll_kraken_timestamp = self._time_synchronizer.time() - order_by_exchange_id_map = {} - for order in self._order_tracker.all_fillable_orders.values(): - order_by_exchange_id_map[order.exchange_order_id] = order - - tasks = [] - trading_pairs = self.trading_pairs - for trading_pair in trading_pairs: - params = { - "symbol": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) - } - if self._last_poll_timestamp > 0: - params["startTime"] = query_time - tasks.append(self._api_get( - path_url=CONSTANTS.MY_TRADES_PATH_URL, - params=params, - is_auth_required=True)) - - self.logger().debug(f"Polling for order fills of {len(tasks)} trading pairs.") - results = await safe_gather(*tasks, return_exceptions=True) - - for trades, trading_pair in zip(results, trading_pairs): - - if isinstance(trades, Exception): - self.logger().network( - f"Error fetching trades update for the order {trading_pair}: {trades}.", - app_warning_msg=f"Failed to fetch trade update for {trading_pair}." - ) - continue - for trade in trades: - exchange_order_id = str(trade["orderId"]) - if exchange_order_id in order_by_exchange_id_map: - # This is a fill for a tracked order - tracked_order = order_by_exchange_id_map[exchange_order_id] - fee = TradeFeeBase.new_spot_fee( - fee_schema=self.trade_fee_schema(), - trade_type=tracked_order.trade_type, - percent_token=trade["commissionAsset"], - flat_fees=[TokenAmount(amount=Decimal(trade["commission"]), token=trade["commissionAsset"])] - ) - trade_update = TradeUpdate( - trade_id=str(trade["id"]), - client_order_id=tracked_order.client_order_id, - exchange_order_id=exchange_order_id, - trading_pair=trading_pair, - fee=fee, - fill_base_amount=Decimal(trade["qty"]), - fill_quote_amount=Decimal(trade["quoteQty"]), - fill_price=Decimal(trade["price"]), - fill_timestamp=trade["time"] * 1e-3, - ) - self._order_tracker.process_trade_update(trade_update) - elif self.is_confirmed_new_order_filled_event(str(trade["id"]), exchange_order_id, trading_pair): - # This is a fill of an order registered in the DB but not tracked any more - self._current_trade_fills.add(TradeFillOrderDetails( - market=self.display_name, - exchange_trade_id=str(trade["id"]), - symbol=trading_pair)) - self.trigger_event( - MarketEvent.OrderFilled, - OrderFilledEvent( - timestamp=float(trade["time"]) * 1e-3, - order_id=self._exchange_order_ids.get(str(trade["orderId"]), None), - trading_pair=trading_pair, - trade_type=TradeType.BUY if trade["isBuyer"] else TradeType.SELL, - order_type=OrderType.LIMIT_MAKER if trade["isMaker"] else OrderType.LIMIT, - price=Decimal(trade["price"]), - amount=Decimal(trade["qty"]), - trade_fee=DeductedFromReturnsTradeFee( - flat_fees=[ - TokenAmount( - trade["commissionAsset"], - Decimal(trade["commission"]) - ) - ] - ), - exchange_trade_id=str(trade["id"]) - )) - self.logger().info(f"Recreating missing trade in TradeFill: {trade}") async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: trade_updates = [] - if order.exchange_order_id is not None: - exchange_order_id = order.exchange_order_id - trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=order.trading_pair) - all_fills_response = await self._api_get( - path_url=CONSTANTS.MY_TRADES_PATH_URL, - params={ - "symbol": trading_pair, - "orderId": exchange_order_id - }, - is_auth_required=True, - limit_id=CONSTANTS.MY_TRADES_PATH_URL) - - for trade in all_fills_response: - exchange_order_id = str(trade["orderId"]) - fee = TradeFeeBase.new_spot_fee( - fee_schema=self.trade_fee_schema(), - trade_type=order.trade_type, - percent_token=trade["commissionAsset"], - flat_fees=[TokenAmount(amount=Decimal(trade["commission"]), token=trade["commissionAsset"])] - ) - trade_update = TradeUpdate( - trade_id=str(trade["id"]), - client_order_id=order.client_order_id, - exchange_order_id=exchange_order_id, - trading_pair=trading_pair, - fee=fee, - fill_base_amount=Decimal(trade["qty"]), - fill_quote_amount=Decimal(trade["quoteQty"]), - fill_price=Decimal(trade["price"]), - fill_timestamp=trade["time"] * 1e-3, - ) + try: + exchange_order_id = await order.get_exchange_order_id() + all_fills_response = await self._api_request_with_retry( + method=RESTMethod.POST, + endpoint=CONSTANTS.QUERY_TRADES_PATH_URL, + data={"txid": exchange_order_id}, + is_auth_required=True) + + for trade_fill in all_fills_response.values(): + trade_update = self._create_trade_update_with_order_fill_data( + order_fill=trade_fill, + order=order) trade_updates.append(trade_update) + except asyncio.TimeoutError: + raise IOError(f"Skipped order update with order fills for {order.client_order_id} " + "- waiting for exchange order id.") + return trade_updates async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: - trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=tracked_order.trading_pair) - updated_order_data = await self._api_get( - path_url=CONSTANTS.ORDER_PATH_URL, - params={ - "symbol": trading_pair, - "origClientOrderId": tracked_order.client_order_id}, + updated_order_data = await self._api_request_with_retry( + method=RESTMethod.POST, + endpoint=CONSTANTS.QUERY_ORDERS_PATH_URL, + params={"txid": tracked_order.exchange_order_id}, is_auth_required=True) - new_state = CONSTANTS.ORDER_STATE[updated_order_data["status"]] + update = updated_order_data.get(tracked_order.exchange_order_id) + + new_state = CONSTANTS.ORDER_STATE[update["status"]] order_update = OrderUpdate( client_order_id=tracked_order.client_order_id, - exchange_order_id=str(updated_order_data["orderId"]), + exchange_order_id=tracked_order.exchange_order_id, trading_pair=tracked_order.trading_pair, - update_timestamp=updated_order_data["updateTime"] * 1e-3, + update_timestamp=self._current_timestamp, new_state=new_state, ) @@ -801,13 +682,13 @@ def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dic async def _get_last_traded_price(self, trading_pair: str) -> float: params = { - "symbol": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) + "pair": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) } resp_json = await self._api_request( method=RESTMethod.GET, - path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, + path_url=CONSTANTS.TICKER_PATH_URL, params=params ) - - return float(resp_json["lastPrice"]) + record = list(resp_json["result"].values())[0] + return float(record["c"][0]) diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index 0d582342de..77a1314af1 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -139,10 +139,6 @@ def attributes(self) -> Tuple[Any]: self.last_update_timestamp, ) ) - # todo - @property - def is_local(self) -> bool: - return self.last_state in {"local"} @classmethod def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": From fa7ae196ca353c040f1b6aae8c5cbf2158eb604e Mon Sep 17 00:00:00 2001 From: bczhang Date: Wed, 7 Feb 2024 19:47:35 +0800 Subject: [PATCH 08/34] finish kraken first version --- .../kraken_api_order_book_data_source.py | 91 +----------- .../kraken_api_user_stream_data_source.py | 7 +- .../connector/exchange/kraken/kraken_auth.py | 7 +- .../exchange/kraken/kraken_constants.py | 12 +- .../exchange/kraken/kraken_exchange.py | 138 ++++++++---------- .../exchange/kraken/kraken_in_fight_order.py | 84 +---------- .../exchange/kraken/kraken_order_book.py | 1 - .../connector/exchange/kraken/kraken_utils.py | 59 +++----- .../exchange/kraken/kraken_web_utils.py | 57 ++------ 9 files changed, 112 insertions(+), 344 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py index 9d0346b9b2..71153c13ac 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py @@ -1,26 +1,17 @@ import asyncio -import logging import time from typing import TYPE_CHECKING, Any, Dict, List, Optional -import pandas as pd - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils - from hummingbot.connector.exchange.kraken.kraken_utils import ( - build_api_factory, - build_rate_limits_by_tier, convert_from_exchange_trading_pair, convert_to_exchange_trading_pair, ) -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.data_type.order_book import OrderBook 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.utils.async_utils import safe_gather -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest +from hummingbot.core.web_assistant.connections.data_types import RESTMethod, WSJSONRequest from hummingbot.core.web_assistant.rest_assistant import RESTAssistant from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory from hummingbot.core.web_assistant.ws_assistant import WSAssistant @@ -32,6 +23,7 @@ class KrakenAPIOrderBookDataSource(OrderBookTrackerDataSource): MESSAGE_TIMEOUT = 30.0 + # PING_TIMEOUT = 10.0 def __init__(self, @@ -42,8 +34,6 @@ def __init__(self, ): super().__init__(trading_pairs) self._connector = connector - # self._throttler = throttler or self._get_throttler_instance() - # self._api_factory = api_factory or build_api_factory(throttler=throttler) self._api_factory = api_factory self._rest_assistant = None self._ws_assistant = None @@ -51,50 +41,16 @@ def __init__(self, _kraobds_logger: Optional[HummingbotLogger] = None - # @classmethod - # def _get_throttler_instance(cls) -> AsyncThrottler: - # throttler = AsyncThrottler(build_rate_limits_by_tier()) - # return throttler async def _get_rest_assistant(self) -> RESTAssistant: if self._rest_assistant is None: self._rest_assistant = await self._api_factory.get_rest_assistant() return self._rest_assistant - # todo async def get_last_traded_prices(self, trading_pairs: List[str], domain: Optional[str] = None) -> Dict[str, float]: return await self._connector.get_last_traded_prices(trading_pairs=trading_pairs) - # - # @classmethod - # async def get_last_traded_prices( - # cls, trading_pairs: List[str], throttler: Optional[AsyncThrottler] = None - # ) -> Dict[str, float]: - # throttler = throttler or cls._get_throttler_instance() - # tasks = [cls._get_last_traded_price(t_pair, throttler) for t_pair in trading_pairs] - # results = await safe_gather(*tasks) - # return {t_pair: result for t_pair, result in zip(trading_pairs, results)} - # - # @classmethod - # async def _get_last_traded_price(cls, trading_pair: str, throttler: AsyncThrottler) -> float: - # url = ( - # f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" - # f"?pair={convert_to_exchange_trading_pair(trading_pair)}" - # ) - # - # request = RESTRequest( - # method=RESTMethod.GET, - # url=url - # ) - # rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - # - # async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL): - # resp = await rest_assistant.call(request) - # resp_json = await resp.json() - # record = list(resp_json["result"].values())[0] - # return float(record["c"][0]) - async def _order_book_snapshot(self, trading_pair: str) -> OrderBook: snapshot: Dict[str, Any] = await self._request_order_book_snapshot(trading_pair) snapshot_timestamp: float = time.time() @@ -114,7 +70,7 @@ async def _request_order_book_snapshot(self, trading_pair: str, ) -> Dict[str, A :return: the response from the exchange (JSON dictionary) """ params = { - "pair": convert_to_exchange_trading_pair(trading_pair) + "pair": await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair) } rest_assistant = await self._api_factory.get_rest_assistant() @@ -132,36 +88,6 @@ async def _request_order_book_snapshot(self, trading_pair: str, ) -> Dict[str, A data["latest_update"] = max([*map(lambda x: x[2], data["bids"] + data["asks"])], default=0.) return data - # @classmethod - # async def fetch_trading_pairs(cls, throttler: Optional[AsyncThrottler] = None) -> List[str]: - # throttler = throttler or cls._get_throttler_instance() - # try: - # async with throttler.execute_task(CONSTANTS.ASSET_PAIRS_PATH_URL): - # url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - # request = RESTRequest( - # method=RESTMethod.GET, - # url=url - # ) - # rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - # response = await rest_assistant.call(request, timeout=5) - # - # if response.status == 200: - # data: Dict[str, Any] = await response.json() - # raw_pairs = data.get("result", []) - # converted_pairs: List[str] = [] - # for pair, details in raw_pairs.items(): - # if "." not in pair: - # try: - # wsname = details["wsname"] # pair in format BASE/QUOTE - # converted_pairs.append(convert_from_exchange_trading_pair(wsname)) - # except IOError: - # pass - # return [item for item in converted_pairs] - # except Exception: - # pass - # # Do nothing if the request fails -- there will be no autocomplete for kraken trading pairs - # return [] - async def _subscribe_channels(self, ws: WSAssistant): """ Subscribes to the trade events and diff orders events through the provided websocket connection. @@ -172,7 +98,7 @@ async def _subscribe_channels(self, ws: WSAssistant): trading_pairs: List[str] = [] for tp in self._trading_pairs: # trading_pairs.append(convert_to_exchange_trading_pair(tp, '/')) - symbol = await self._connector.exchange_symbol_associated_to_pair(trading_pair=tp) + symbol = convert_to_exchange_trading_pair(tp, '/') trading_pairs.append(symbol) trades_payload = { "event": "subscribe", @@ -215,22 +141,18 @@ async def _connected_websocket_assistant(self) -> WSAssistant: ping_timeout=CONSTANTS.PING_TIMEOUT) return ws - # todo 把convert_from_exchange_trading_pair改掉 async def _parse_trade_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue): trades = [ - # {"pair": convert_from_exchange_trading_pair(raw_message[-1]), "trade": trade} - {"pair": await self._connector.exchange_symbol_associated_to_pair(raw_message[-1]), "trade": trade} + {"pair": convert_from_exchange_trading_pair(raw_message[-1]), "trade": trade} for trade in raw_message[1] ] for trade in trades: trade_msg: OrderBookMessage = KrakenOrderBook.trade_message_from_exchange(trade) message_queue.put_nowait(trade_msg) - async def _parse_order_book_diff_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue): - # msg_dict = {"trading_pair": convert_from_exchange_trading_pair(raw_message[-1]), - msg_dict = {"trading_pair": await self._connector.exchange_symbol_associated_to_pair(raw_message[-1]), + msg_dict = {"trading_pair": convert_from_exchange_trading_pair(raw_message[-1]), "asks": raw_message[1].get("a", []) or raw_message[1].get("as", []) or [], "bids": raw_message[1].get("b", []) or raw_message[1].get("bs", []) or []} msg_dict["update_id"] = max( @@ -244,4 +166,3 @@ async def _parse_order_book_diff_message(self, raw_message: Dict[str, Any], mess order_book_message: OrderBookMessage = KrakenOrderBook.diff_message_from_exchange( msg_dict, time.time()) message_queue.put_nowait(order_book_message) - diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index 918589dafa..cf7621021d 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -2,14 +2,12 @@ from typing import Any, Dict, Optional, TYPE_CHECKING from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.core.web_assistant.connections.data_types import WSJSONRequest from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory from hummingbot.core.web_assistant.ws_assistant import WSAssistant from hummingbot.logger import HummingbotLogger - if TYPE_CHECKING: from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange @@ -31,7 +29,6 @@ async def _connected_websocket_assistant(self) -> WSAssistant: await ws.connect(ws_url=CONSTANTS.WS_AUTH_URL, ping_timeout=CONSTANTS.PING_TIMEOUT) return ws - @property def last_recv_time(self): if self._ws_assistant is None: @@ -39,11 +36,10 @@ def last_recv_time(self): else: return self._ws_assistant.last_recv_time - async def get_auth_token(self) -> str: try: response_json = await self._connector._api_post(path_url=CONSTANTS.GET_TOKEN_PATH_URL, params={}, - is_auth_required=True) + is_auth_required=True) except Exception: raise IOError(f"Error parsing data from {CONSTANTS.GET_TOKEN_PATH_URL}.") @@ -56,7 +52,6 @@ async def get_auth_token(self) -> str: return response_json["result"]["token"] - async def _subscribe_channels(self, websocket_assistant: WSAssistant): """ Subscribes to order events and balance events. diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index 68bfba080a..5b68fcb194 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -10,10 +10,7 @@ from hummingbot.connector.time_synchronizer import TimeSynchronizer from hummingbot.core.web_assistant.auth import AuthBase -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSRequest - - - +from hummingbot.core.web_assistant.connections.data_types import RESTRequest, WSRequest class KrakenAuth(AuthBase): @@ -39,7 +36,7 @@ async def rest_authenticate(self, request: RESTRequest) -> RESTRequest: request.headers = headers request.data = auth_dict["postDict"] return request - #todo + async def ws_authenticate(self, request: WSRequest) -> WSRequest: """ This method is intended to configure a websocket request to be authenticated. Mexc does not use this diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index 38aee3e2b7..1fae427aea 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -6,10 +6,11 @@ from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair from hummingbot.core.data_type.in_flight_order import OrderState -DEFAULT_DOMAIN="kraken" +DEFAULT_DOMAIN = "kraken" MAX_ORDER_ID_LEN = 32 HBOT_ORDER_ID_PREFIX = "HBOT" + class KrakenAPITier(Enum): """ Kraken's Private Endpoint Rate Limit Tiers, based on the Account Verification level. @@ -54,7 +55,6 @@ class KrakenAPITier(Enum): QUERY_ORDERS_PATH_URL = "/0/private/QueryOrders" QUERY_TRADES_PATH_URL = "/0/private/QueryTrades" - # Order States ORDER_STATE = { "pending": OrderState.PENDING_CREATE, @@ -62,14 +62,8 @@ class KrakenAPITier(Enum): "closed": OrderState.COMPLETED, "canceled": OrderState.CANCELED, "expired": OrderState.FAILED, - - "FILLED": OrderState.FILLED, - "PARTIALLY_FILLED": OrderState.PARTIALLY_FILLED, - "PENDING_CANCEL": OrderState.OPEN, - "PARTIALLY_CANCELED": OrderState.CANCELED, - "REJECTED": OrderState.FAILED, - "EXPIRED": OrderState.FAILED, } +ORDER_NOT_EXIST_ERROR_CODE = "Error fetching status update for the order" WS_URL = "wss://ws.kraken.com" WS_AUTH_URL = "wss://ws-auth.kraken.com/" diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index a90c09d00d..2e8fe0801a 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -7,16 +7,12 @@ from bidict import bidict from hummingbot.connector.constants import s_decimal_NaN -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_utils, \ +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, \ kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_utils import ( - build_api_factory, build_rate_limits_by_tier, convert_from_exchange_symbol, convert_from_exchange_trading_pair, - convert_to_exchange_trading_pair, - is_dark_pool, - split_to_base_quote, ) from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource @@ -24,20 +20,17 @@ from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.connector.exchange.kraken.kraken_in_fight_order import ( KrakenInFlightOrder, - # KrakenInFlightOrderNotCreated, ) from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import TradeFillOrderDetails, combine_to_hb_trading_pair, get_new_client_order_id +from hummingbot.connector.utils import combine_to_hb_trading_pair, get_new_client_order_id from hummingbot.core.api_throttler.async_throttler import AsyncThrottler 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.order_book_tracker_data_source import OrderBookTrackerDataSource from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource -from hummingbot.core.event.events import MarketEvent, OrderFilledEvent -from hummingbot.core.utils.async_utils import safe_gather, safe_ensure_future -from hummingbot.core.utils.tracking_nonce import get_tracking_nonce +from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.core.web_assistant.connections.data_types import RESTMethod from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -94,10 +87,10 @@ def authenticator(self): def name(self) -> str: return "kraken" - # todo - # @property - # def rate_limits_rules(self): - # return CONSTANTS.RATE_LIMITS + # not used + @property + def rate_limits_rules(self): + return build_rate_limits_by_tier(self._kraken_api_tier) @property def domain(self): @@ -127,7 +120,6 @@ def check_network_request_path(self): def trading_pairs(self): return self._trading_pairs - # todo @property def is_cancel_request_in_exchange_synchronous(self) -> bool: return True @@ -153,30 +145,18 @@ def _build_async_throttler(self, api_tier: KrakenAPITier) -> AsyncThrottler: throttler = AsyncThrottler(build_rate_limits_by_tier(api_tier)) return throttler - # todo def _is_request_exception_related_to_time_synchronizer(self, request_exception: Exception): - error_description = str(request_exception) - is_time_synchronizer_related = ("-1021" in error_description - and "Timestamp for this request" in error_description) - return is_time_synchronizer_related + return False - # todo def _is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool: - return str(CONSTANTS.ORDER_NOT_EXIST_ERROR_CODE) in str( - status_update_exception - ) and CONSTANTS.ORDER_NOT_EXIST_MESSAGE in str(status_update_exception) + return str(CONSTANTS.ORDER_NOT_EXIST_ERROR_CODE) in str(status_update_exception) - # todo def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: - return str(CONSTANTS.UNKNOWN_ORDER_ERROR_CODE) in str( - cancelation_exception - ) and CONSTANTS.UNKNOWN_ORDER_MESSAGE in str(cancelation_exception) + return False def _create_web_assistants_factory(self) -> WebAssistantsFactory: return web_utils.build_api_factory( throttler=self._throttler, - time_synchronizer=self._time_synchronizer, - domain=self._domain, auth=self._auth) def _create_order_book_data_source(self) -> OrderBookTrackerDataSource: @@ -291,7 +271,8 @@ async def get_asset_pairs(self) -> Dict[str, Any]: if not self._asset_pairs: asset_pairs = await self._api_request(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) self._asset_pairs = {f"{details['base']}-{details['quote']}": details - for _, details in asset_pairs.items() if not is_dark_pool(details)} + for _, details in asset_pairs.items() if + web_utils.is_exchange_information_valid(details)} return self._asset_pairs def start_tracking_order(self, @@ -348,7 +329,7 @@ async def _place_order(self, price: Decimal, **kwargs) -> Tuple[str, float]: userref = kwargs.get("userref", 0) - trading_pair = convert_to_exchange_trading_pair(trading_pair) + trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) data = { "pair": trading_pair, "type": "buy" if trade_type is TradeType.BUY else "sell", @@ -487,7 +468,6 @@ async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> Lis self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True) return retval - async def _update_trading_fees(self): """ Update fees information from the exchange @@ -502,23 +482,15 @@ async def _user_stream_event_listener(self): user_channels = [ CONSTANTS.USER_TRADES_ENDPOINT_NAME, CONSTANTS.USER_ORDERS_ENDPOINT_NAME, - CONSTANTS.USER_BALANCE_ENDPOINT_NAME, ] async for event_message in self._iter_user_event_queue(): try: - channel: str = event_message.get("c", None) - results: Dict[str, Any] = event_message.get("d", {}) - if "code" not in event_message and channel not in user_channels: - self.logger().error( - f"Unexpected message in user stream: {event_message}.", exc_info=True) - continue + channel: str = event_message[-2] + results: List[Any] = event_message[0] if channel == CONSTANTS.USER_TRADES_ENDPOINT_NAME: self._process_trade_message(results) elif channel == CONSTANTS.USER_ORDERS_ENDPOINT_NAME: self._process_order_message(event_message) - elif channel == CONSTANTS.USER_BALANCE_ENDPOINT_NAME: - self._process_balance_message_ws(results) - except asyncio.CancelledError: raise except Exception: @@ -559,39 +531,48 @@ def _create_trade_update_with_order_fill_data( ) return trade_update - def _process_trade_message(self, trade: Dict[str, Any], client_order_id: Optional[str] = None): - client_order_id = client_order_id or str(trade["c"]) - tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) - if tracked_order is None: - self.logger().debug(f"Ignoring trade message with id {client_order_id}: not in in_flight_orders.") - else: - trade_update = self._create_trade_update_with_order_fill_data( - order_fill=trade, - order=tracked_order) - self._order_tracker.process_trade_update(trade_update) + def _process_trade_message(self, trades: List): + for update in trades: + trade_id: str = next(iter(update)) + trade: Dict[str, str] = update[trade_id] + trade["trade_id"] = trade_id + exchange_order_id = trade.get("ordertxid") + try: + client_order_id = next(key for key, value in self._in_flight_orders.items() + if value.exchange_order_id == exchange_order_id) + except StopIteration: + continue + + tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) + if tracked_order is None: + self.logger().debug(f"Ignoring trade message with id {client_order_id}: not in in_flight_orders.") + else: + trade_update = self._create_trade_update_with_order_fill_data( + order_fill=trade, + order=tracked_order) + self._order_tracker.process_trade_update(trade_update) def _create_order_update_with_order_status_data(self, order_status: Dict[str, Any], order: InFlightOrder): - client_order_id = str(order_status["d"].get("c", "")) order_update = OrderUpdate( trading_pair=order.trading_pair, - update_timestamp=int(order_status["t"] * 1e-3), - new_state=CONSTANTS.WS_ORDER_STATE[order_status["d"]["s"]], - client_order_id=client_order_id, - exchange_order_id=str(order_status["d"]["i"]), + update_timestamp=self.current_timestamp, + new_state=CONSTANTS.ORDER_STATE[order_status["status"]], + client_order_id=order.client_order_id, + exchange_order_id=order.exchange_order_id, ) return order_update - def _process_order_message(self, raw_msg: Dict[str, Any]): - order_msg = raw_msg.get("d", {}) - client_order_id = str(order_msg.get("c", "")) - tracked_order = self._order_tracker.all_updatable_orders.get(client_order_id) - if not tracked_order: - self.logger().debug(f"Ignoring order message with id {client_order_id}: not in in_flight_orders.") - return - - order_update = self._create_order_update_with_order_status_data(order_status=raw_msg, order=tracked_order) - self._order_tracker.process_order_update(order_update=order_update) - + def _process_order_message(self, orders: List): + for update in orders: + for exchange_order_id, order_msg in update.items(): + tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) + if not tracked_order: + self.logger().debug( + f"Ignoring order message with id {tracked_order.client_order_id}: not in in_flight_orders.") + return + order_update = self._create_order_update_with_order_status_data(order_status=order_msg, + order=tracked_order) + self._order_tracker.process_order_update(order_update=order_update) async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: trade_updates = [] @@ -604,9 +585,11 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade data={"txid": exchange_order_id}, is_auth_required=True) - for trade_fill in all_fills_response.values(): + for trade_id, trade_fill in all_fills_response.items(): + trade: Dict[str, str] = all_fills_response[trade_id] + trade["trade_id"] = trade_id trade_update = self._create_trade_update_with_order_fill_data( - order_fill=trade_fill, + order_fill=trade, order=order) trade_updates.append(trade_update) @@ -625,6 +608,10 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda update = updated_order_data.get(tracked_order.exchange_order_id) + if update.get("error") is not None and "EOrder:Invalid order" not in update["error"]: + self.logger().debug(f"Error in fetched status update for order {tracked_order.client_order_id}: " + f"{update['error']}") + await self._place_cancel(tracked_order.client_order_id, tracked_order) new_state = CONSTANTS.ORDER_STATE[update["status"]] order_update = OrderUpdate( @@ -640,8 +627,10 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda async def _update_balances(self): local_asset_names = set(self._account_balances.keys()) remote_asset_names = set() - balances = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.BALANCE_PATH_URL, is_auth_required=True) - open_orders = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.OPEN_ORDERS_PATH_URL, is_auth_required=True) + balances = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.BALANCE_PATH_URL, + is_auth_required=True) + open_orders = await self._api_request_with_retry(RESTMethod.POST, CONSTANTS.OPEN_ORDERS_PATH_URL, + is_auth_required=True) locked = defaultdict(Decimal) @@ -672,12 +661,11 @@ async def _update_balances(self): del self._account_available_balances[asset_name] del self._account_balances[asset_name] - def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]): mapping = bidict() for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info.values()): mapping[symbol_data["altname"]] = combine_to_hb_trading_pair(base=symbol_data["base"], - quote=symbol_data["quote"]) + quote=symbol_data["quote"]) self._set_trading_pair_symbol_map(mapping) async def _get_last_traded_price(self, trading_pair: str) -> float: diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index 77a1314af1..de7cf56722 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -1,89 +1,9 @@ -import asyncio import copy -import math -import typing from decimal import Decimal -from enum import Enum -from typing import Any, Dict, NamedTuple, Optional, Tuple - -from async_timeout import timeout +from typing import Any, Dict, Optional, Tuple from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType -from hummingbot.core.data_type.limit_order import LimitOrder -from hummingbot.core.data_type.trade_fee import TradeFeeBase -from hummingbot.core.data_type.in_flight_order import InFlightOrder -if typing.TYPE_CHECKING: # avoid circular import problems - from hummingbot.connector.exchange_base import ExchangeBase - -s_decimal_0 = Decimal("0") - -GET_EX_ORDER_ID_TIMEOUT = 10 # seconds - -# todo -class OrderState(Enum): - PENDING_CREATE = 0 - OPEN = 1 - PENDING_CANCEL = 2 - CANCELED = 3 - PARTIALLY_FILLED = 4 - FILLED = 5 - FAILED = 6 - PENDING_APPROVAL = 7 - APPROVED = 8 - CREATED = 9 - COMPLETED = 10 - - -class OrderUpdate(NamedTuple): - trading_pair: str - update_timestamp: float # seconds - new_state: OrderState - client_order_id: Optional[str] = None - exchange_order_id: Optional[str] = None - misc_updates: Optional[Dict[str, Any]] = None - - -class TradeUpdate(NamedTuple): - trade_id: str - client_order_id: str - exchange_order_id: str - trading_pair: str - fill_timestamp: float # seconds - fill_price: Decimal - fill_base_amount: Decimal - fill_quote_amount: Decimal - fee: TradeFeeBase - is_taker: bool = True # CEXs deliver trade events from the taker's perspective - - @property - def fee_asset(self): - return self.fee.fee_asset - - @classmethod - def from_json(cls, data: Dict[str, Any]): - instance = TradeUpdate( - trade_id=data["trade_id"], - client_order_id=data["client_order_id"], - exchange_order_id=data["exchange_order_id"], - trading_pair=data["trading_pair"], - fill_timestamp=data["fill_timestamp"], - fill_price=Decimal(data["fill_price"]), - fill_base_amount=Decimal(data["fill_base_amount"]), - fill_quote_amount=Decimal(data["fill_quote_amount"]), - fee=TradeFeeBase.from_json(data["fee"]), - ) - - return instance - - def to_json(self) -> Dict[str, Any]: - json_dict = self._asdict() - json_dict.update({ - "fill_price": str(self.fill_price), - "fill_base_amount": str(self.fill_base_amount), - "fill_quote_amount": str(self.fill_quote_amount), - "fee": self.fee.to_json(), - }) - return json_dict +from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate class KrakenInFlightOrder(InFlightOrder): diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.py b/hummingbot/connector/exchange/kraken/kraken_order_book.py index 801f1afe99..328b6c257f 100644 --- a/hummingbot/connector/exchange/kraken/kraken_order_book.py +++ b/hummingbot/connector/exchange/kraken/kraken_order_book.py @@ -1,4 +1,3 @@ -import logging from typing import ( Dict, Optional diff --git a/hummingbot/connector/exchange/kraken/kraken_utils.py b/hummingbot/connector/exchange/kraken/kraken_utils.py index d00c27842b..7573e350fe 100644 --- a/hummingbot/connector/exchange/kraken/kraken_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_utils.py @@ -1,23 +1,23 @@ -from typing import Any, Dict, List, Optional, Tuple +from decimal import Decimal +from typing import List, Optional, Tuple from pydantic import Field, SecretStr +from pydantic.class_validators import validator import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS from hummingbot.client.config.config_data_types import BaseConnectorConfigMap, ClientFieldData from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory +from hummingbot.core.data_type.trade_fee import TradeFeeSchema CENTRALIZED = True EXAMPLE_PAIR = "ETH-USDC" -DEFAULT_FEES = [0.16, 0.26] - - -def split_trading_pair(trading_pair: str) -> Tuple[str, str]: - return tuple(convert_from_exchange_trading_pair(trading_pair).split("-")) +DEFAULT_FEES = TradeFeeSchema( + maker_percent_fee_decimal=Decimal("0.16"), + taker_percent_fee_decimal=Decimal("0.26"), +) def convert_from_exchange_symbol(symbol: str) -> str: @@ -37,7 +37,8 @@ def split_to_base_quote(exchange_trading_pair: str) -> Tuple[Optional[str], Opti return base, quote -def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_trading_pairs: Optional[Tuple] = None) -> Optional[str]: +def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_trading_pairs: Optional[Tuple] = None) -> \ +Optional[str]: base, quote = "", "" if "-" in exchange_trading_pair: base, quote = split_to_base_quote(exchange_trading_pair) @@ -46,7 +47,8 @@ def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_tra elif len(available_trading_pairs) > 0: # If trading pair has no spaces (i.e. ETHUSDT). Then it will have to match with the existing pairs # Option 1: Using traditional naming convention - connector_trading_pair = {''.join(convert_from_exchange_trading_pair(tp).split('-')): tp for tp in available_trading_pairs}.get( + connector_trading_pair = {''.join(convert_from_exchange_trading_pair(tp).split('-')): tp for tp in + available_trading_pairs}.get( exchange_trading_pair) if not connector_trading_pair: # Option 2: Using kraken naming convention ( XXBT for Bitcoin, XXDG for Doge, ZUSD for USD, etc) @@ -84,17 +86,6 @@ def convert_to_exchange_trading_pair(hb_trading_pair: str, delimiter: str = "") return exchange_trading_pair -def is_dark_pool(trading_pair_details: Dict[str, Any]): - ''' - Want to filter out dark pool trading pairs from the list of trading pairs - For more info, please check - https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool - ''' - if trading_pair_details.get('altname'): - return trading_pair_details.get('altname').endswith('.d') - return False - - def _build_private_rate_limits(tier: KrakenAPITier = KrakenAPITier.STARTER) -> List[RateLimit]: private_rate_limits = [] @@ -166,16 +157,6 @@ def build_rate_limits_by_tier(tier: KrakenAPITier = KrakenAPITier.STARTER) -> Li return rate_limits -def _api_tier_validator(value: str) -> Optional[str]: - """ - Determines if input value is a valid API tier - """ - try: - KrakenAPITier(value.upper()) - except ValueError: - return "No such Kraken API Tier." - - class KrakenConfigMap(BaseConnectorConfigMap): connector: str = Field(default="kraken", client_data=None) kraken_api_key: SecretStr = Field( @@ -208,10 +189,16 @@ class KrakenConfigMap(BaseConnectorConfigMap): class Config: title = "kraken" + @validator("kraken_api_tier", pre=True) + def _api_tier_validator(cls, value: str) -> Optional[str]: + """ + Determines if input value is a valid API tier + """ + try: + KrakenAPITier(value.upper()) + return value + except ValueError: + raise ValueError("No such Kraken API Tier.") -KEYS = KrakenConfigMap.construct() - -def build_api_factory(throttler: AsyncThrottler) -> WebAssistantsFactory: - api_factory = WebAssistantsFactory(throttler=throttler) - return api_factory +KEYS = KrakenConfigMap.construct() diff --git a/hummingbot/connector/exchange/kraken/kraken_web_utils.py b/hummingbot/connector/exchange/kraken/kraken_web_utils.py index 9ad5ad8781..fb49f9be55 100644 --- a/hummingbot/connector/exchange/kraken/kraken_web_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_web_utils.py @@ -1,11 +1,8 @@ -from typing import Callable, Optional +from typing import Optional import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS -from hummingbot.connector.time_synchronizer import TimeSynchronizer -from hummingbot.connector.utils import TimeSynchronizerRESTPreProcessor from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.web_assistant.auth import AuthBase -from hummingbot.core.web_assistant.connections.data_types import RESTMethod from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -18,62 +15,32 @@ def public_rest_url(*args, **kwargs) -> str: def rest_url(path_url: str, domain: str = "kraken"): - # base_url = CONSTANTS.BASE_URL if domain == "kraken" else CONSTANTS.TESTNET_BASE_URL base_url = CONSTANTS.BASE_URL return base_url + path_url + def build_api_factory( throttler: Optional[AsyncThrottler] = None, - time_synchronizer: Optional[TimeSynchronizer] = None, - domain: str = CONSTANTS.DEFAULT_DOMAIN, - time_provider: Optional[Callable] = None, auth: Optional[AuthBase] = None, ) -> WebAssistantsFactory: - throttler = throttler or create_throttler() - time_synchronizer = time_synchronizer or TimeSynchronizer() - time_provider = time_provider or (lambda: get_current_server_time( - throttler=throttler, - domain=domain, - )) + throttler = throttler api_factory = WebAssistantsFactory( throttler=throttler, - auth=auth, - rest_pre_processors=[ - TimeSynchronizerRESTPreProcessor(synchronizer=time_synchronizer, time_provider=time_provider), - ]) - return api_factory - - -def build_api_factory_without_time_synchronizer_pre_processor(throttler: AsyncThrottler) -> WebAssistantsFactory: - api_factory = WebAssistantsFactory(throttler=throttler) - return api_factory - - -def create_throttler() -> AsyncThrottler: - return AsyncThrottler(CONSTANTS.RATE_LIMITS) - - -async def get_current_server_time( - throttler: Optional[AsyncThrottler] = None, - domain: str = CONSTANTS.DEFAULT_DOMAIN, -) -> float: - throttler = throttler or create_throttler() - api_factory = build_api_factory_without_time_synchronizer_pre_processor(throttler=throttler) - rest_assistant = await api_factory.get_rest_assistant() - response = await rest_assistant.execute_request( - url=public_rest_url(path_url=CONSTANTS.SERVER_TIME_PATH_URL, domain=domain), - method=RESTMethod.GET, - throttler_limit_id=CONSTANTS.SERVER_TIME_PATH_URL, + auth=auth ) - server_time = response["serverTime"] - return server_time + return api_factory -def is_exchange_information_valid(rule) -> bool: +def is_exchange_information_valid(trading_pair_details) -> bool: """ Verifies if a trading pair is enabled to operate with based on its exchange information :param exchange_info: the exchange information for a trading pair :return: True if the trading pair is enabled, False otherwise + Want to filter out dark pool trading pairs from the list of trading pairs + For more info, please check + https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool """ - return True \ No newline at end of file + if trading_pair_details.get('altname'): + return not trading_pair_details.get('altname').endswith('.d') + return True From 869fc3b1e4976295aaf964d9af02bb56a88aad03 Mon Sep 17 00:00:00 2001 From: bczhang Date: Fri, 9 Feb 2024 00:17:07 +0800 Subject: [PATCH 09/34] add part of unittest --- .../exchange/kraken/kraken_exchange.py | 4 +- .../exchange/kraken/test_kraken_auth.py | 0 .../exchange/kraken/test_kraken_exchange.py | 1553 +++++++++++++---- .../kraken/test_kraken_in_flight_order.py | 22 +- .../exchange/kraken/test_kraken_order_book.py | 96 + .../exchange/kraken/test_kraken_utils.py | 41 + .../exchange/kraken/test_kraken_web_utils.py | 43 + .../connector/exchange/kraken_v1/__init__.py | 0 .../test_kraken_api_order_book_data_source.py | 271 +++ ...test_kraken_api_user_stream_data_source.py | 138 ++ .../kraken_v1/test_kraken_exchange.py | 454 +++++ .../kraken_v1/test_kraken_in_flight_order.py | 90 + 12 files changed, 2335 insertions(+), 377 deletions(-) create mode 100644 test/hummingbot/connector/exchange/kraken/test_kraken_auth.py create mode 100644 test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py create mode 100644 test/hummingbot/connector/exchange/kraken/test_kraken_utils.py create mode 100644 test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py create mode 100644 test/hummingbot/connector/exchange/kraken_v1/__init__.py create mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py create mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py create mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py create mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 2e8fe0801a..c023edaf30 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -48,14 +48,14 @@ class KrakenExchange(ExchangePyBase): def __init__(self, client_config_map: "ClientConfigAdapter", kraken_api_key: str, - kraken_api_secret: str, + kraken_secret_key: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN, kraken_api_tier: str = "starter" ): self.api_key = kraken_api_key - self.secret_key = kraken_api_secret + self.secret_key = kraken_secret_key self._domain = domain self._trading_required = trading_required self._trading_pairs = trading_pairs diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index 6096c57f47..fb7d74dc01 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -1,454 +1,1279 @@ import asyncio import json import re -import unittest from decimal import Decimal -from functools import partial -from typing import Awaitable, Dict +from typing import Any, Callable, Dict, List, Optional, Tuple +from unittest.mock import patch from aioresponses import aioresponses +from aioresponses.core import RequestCall from hummingbot.client.config.client_config_map import ClientConfigMap from hummingbot.client.config.config_helpers import ClientConfigAdapter -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange -from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrderNotCreated -from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant -from hummingbot.core.clock import Clock, ClockMode +from hummingbot.connector.test_support.exchange_connector_test import AbstractExchangeConnectorTests +from hummingbot.connector.trading_rule import TradingRule +from hummingbot.connector.utils import get_new_client_order_id from hummingbot.core.data_type.common import OrderType, TradeType -from hummingbot.core.event.event_logger import EventLogger -from hummingbot.core.event.events import ( - BuyOrderCompletedEvent, - BuyOrderCreatedEvent, - MarketEvent, - OrderCancelledEvent, - SellOrderCreatedEvent, -) -from hummingbot.core.network_iterator import NetworkStatus - - -class KrakenExchangeTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.ev_loop = asyncio.get_event_loop() - cls.base_asset = "COINALPHA" - cls.quote_asset = "HBOT" - cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" - - def setUp(self) -> None: - super().setUp() - self.mocking_assistant = NetworkMockingAssistant() - self.event_listener = EventLogger() - not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" - self.client_config_map = ClientConfigAdapter(ClientConfigMap()) - - self.exchange = KrakenExchange( - client_config_map=self.client_config_map, - kraken_api_key="someKey", - kraken_secret_key=not_a_real_secret, - trading_pairs=[self.trading_pair], - ) - self.start_time = 1 - self.clock = Clock(clock_mode=ClockMode.BACKTEST, start_time=self.start_time) - - def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): - ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) - return ret - - def simulate_trading_rules_initialized(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) - - self.async_run_with_timeout(self.exchange._update_trading_rules(), timeout=2) - - @staticmethod - def register_sent_request(requests_list, url, **kwargs): - requests_list.append((url, kwargs)) - - def get_asset_pairs_mock(self) -> Dict: - asset_pairs = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "altname": f"{self.base_asset}{self.quote_asset}", - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": f"{self.base_asset}", - "aclass_quote": "currency", - "quote": f"{self.quote_asset}", - "lot": "unit", - "pair_decimals": 5, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [ - 2, - 3, - ], - "leverage_sell": [ - 2, - 3, +from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState +from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase +from hummingbot.core.event.events import MarketOrderFailureEvent, OrderFilledEvent + + +class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): + + @property + def all_symbols_url(self): + return web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.exchange._domain) + + @property + def latest_prices_url(self): + url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.exchange._domain) + url = f"{url}?symbol={self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset)}" + return url + + @property + def network_status_url(self): + url = web_utils.private_rest_url(CONSTANTS.PING_PATH_URL, domain=self.exchange._domain) + return url + + @property + def trading_rules_url(self): + url = web_utils.private_rest_url(CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.exchange._domain) + return url + + @property + def order_creation_url(self): + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL, domain=self.exchange._domain) + return url + + @property + def balance_url(self): + url = web_utils.private_rest_url(CONSTANTS.ACCOUNTS_PATH_URL, domain=self.exchange._domain) + return url + + @property + def all_symbols_request_mock_response(self): + return { + "timezone": "UTC", + "serverTime": 1639598493658, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [ + { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "status": "ENABLED", + "baseAsset": self.base_asset, + "baseSizePrecision": 1e-8, + "quotePrecision": 8, + "baseAssetPrecision": 8, + "quoteAmountPrecision": 8, + "quoteAsset": self.quote_asset, + "quoteAssetPrecision": 8, + "baseCommissionPrecision": 8, + "quoteCommissionPrecision": 8, + "orderTypes": [ + "LIMIT", + "LIMIT_MAKER", + "MARKET", + "STOP_LOSS_LIMIT", + "TAKE_PROFIT_LIMIT" ], - "fees": [ - [ - 0, - 0.26 - ], - [ - 50000, - 0.24 - ], + "icebergAllowed": True, + "ocoAllowed": True, + "quoteOrderQtyMarketAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + "filters": [], + "permissions": [ + "SPOT", + "MARGIN" + ] + }, + ] + } + + @property + def latest_prices_request_mock_response(self): + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "priceChange": "-94.99999800", + "priceChangePercent": "-95.960", + "weightedAvgPrice": "0.29628482", + "prevClosePrice": "0.10002000", + "lastPrice": str(self.expected_latest_price), + "lastQty": "200.00000000", + "bidPrice": "4.00000000", + "bidQty": "100.00000000", + "askPrice": "4.00000200", + "askQty": "100.00000000", + "openPrice": "99.00000000", + "highPrice": "100.00000000", + "lowPrice": "0.10000000", + "volume": "8913.30000000", + "quoteVolume": "15.30000000", + "openTime": 1499783499040, + "closeTime": 1499869899040, + "firstId": 28385, + "lastId": 28460, + "count": 76, + } + + @property + def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: + response = { + "timezone": "UTC", + "serverTime": 1639598493658, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [ + { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "status": "ENABLED", + "baseAsset": self.base_asset, + "baseSizePrecision": 1e-8, + "quotePrecision": 8, + "baseAssetPrecision": 8, + "quoteAsset": self.quote_asset, + "quoteAssetPrecision": 8, + "baseCommissionPrecision": 8, + "quoteAmountPrecision": 8, + "quoteCommissionPrecision": 8, + "orderTypes": [ + "LIMIT", + "LIMIT_MAKER", + "MARKET", + "STOP_LOSS_LIMIT", + "TAKE_PROFIT_LIMIT" ], - "fees_maker": [ - [ - 0, - 0.16 - ], - [ - 50000, - 0.14 - ], + "icebergAllowed": True, + "ocoAllowed": True, + "quoteOrderQtyMarketAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + "filters": [], + "permissions": [ + "MARGIN" + ] + }, + { + "symbol": self.exchange_symbol_for_tokens("INVALID", "PAIR"), + "status": "ENABLED", + "baseAsset": "INVALID", + "baseSizePrecision": 1e-8, + "quotePrecision": 8, + "baseAssetPrecision": 8, + "quoteAmountPrecision": 8, + "quoteAsset": "PAIR", + "quoteAssetPrecision": 8, + "baseCommissionPrecision": 8, + "quoteCommissionPrecision": 8, + "orderTypes": [ + "LIMIT", + "LIMIT_MAKER", + "MARKET", + "STOP_LOSS_LIMIT", + "TAKE_PROFIT_LIMIT" ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.005" + "icebergAllowed": True, + "ocoAllowed": True, + "quoteOrderQtyMarketAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + "filters": [], + "permissions": [ + "MARGIN" + ] }, - } + ] } - return asset_pairs - - def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: - balances = { - "error": [], - "result": { - self.base_asset: str(base_asset_balance), - self.quote_asset: str(quote_asset_balance), - "USDT": "171288.6158", - } + + return "INVALID-PAIR", response + + @property + def network_status_request_successful_mock_response(self): + return {} + + @property + def trading_rules_request_mock_response(self): + return { + "timezone": "UTC", + "serverTime": 1565246363776, + "rateLimits": [{}], + "exchangeFilters": [], + "symbols": [ + { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "status": "ENABLED", + "baseAsset": self.base_asset, + "baseSizePrecision": 1e-8, + "quotePrecision": 8, + "baseAssetPrecision": 8, + "quoteAmountPrecision": 8, + "quoteAsset": self.quote_asset, + "quoteAssetPrecision": 8, + "orderTypes": ["LIMIT", "LIMIT_MAKER"], + "icebergAllowed": True, + "ocoAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + }, { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "200000.00000000", + "stepSize": "0.00100000" + }, { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00200000" + } + ], + "permissions": [ + "SPOT", + "MARGIN" + ] + } + ] } - return balances - - def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: - open_orders = { - "error": [], - "result": { - "open": { - "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), + + @property + def trading_rules_request_erroneous_mock_response(self): + return { + "timezone": "UTC", + "serverTime": 1565246363776, + "rateLimits": [{}], + "exchangeFilters": [], + "symbols": [ + { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "status": "ENABLED", + "baseAsset": self.base_asset, + "baseAssetPrecision": 8, + "quoteAsset": self.quote_asset, + "quotePrecision": 8, + "quoteAssetPrecision": 8, + "orderTypes": ["LIMIT", "LIMIT_MAKER"], + "icebergAllowed": True, + "ocoAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + "permissions": [ + "SPOT", + "MARGIN" + ] } - } + ] } - return open_orders - - def get_query_orders_mock( - self, exchange_id: str, quantity: float, price: float, order_type: str, status: str - ) -> Dict: - query_orders = { - "error": [], - "result": { - exchange_id: self.get_order_status_mock(quantity, price, order_type, status) - } + + @property + def order_creation_request_successful_mock_response(self): + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": self.expected_exchange_order_id, + "orderListId": -1, + "clientOrderId": "OID1", + "transactTime": 1507725176595 } - return query_orders - def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: - order_status = { - "refid": None, - "userref": 0, - "status": status, - "opentm": 1616666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": { - "pair": f"{self.base_asset}{self.quote_asset}", - "type": order_type, - "ordertype": "limit", - "price": str(price), - "price2": "0", - "leverage": "none", - "order": f"buy {quantity} {self.base_asset}{self.quote_asset} @ limit {price}", - "close": "" - }, - "vol": str(quantity), - "vol_exec": "0", - "cost": str(price * quantity), - "fee": "0.00000", - "price": str(price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [ - "TCCCTY-WE2O6-P3NB37" + @property + def balance_request_mock_response_for_base_and_quote(self): + return { + "makerCommission": 15, + "takerCommission": 15, + "buyerCommission": 0, + "sellerCommission": 0, + "canTrade": True, + "canWithdraw": True, + "canDeposit": True, + "updateTime": 123456789, + "accountType": "SPOT", + "balances": [ + { + "asset": self.base_asset, + "free": "10.0", + "locked": "5.0" + }, + { + "asset": self.quote_asset, + "free": "2000", + "locked": "0.00000000" + } + ], + "permissions": [ + "SPOT" ] } - return order_status - - def get_order_placed_mock(self, exchange_id: str, quantity: float, price: float, order_type: str) -> Dict: - order_placed = { - "error": [], - "result": { - "descr": { - "order": f"{order_type} {quantity} {self.base_asset}{self.quote_asset}" - f" @ limit {price} with 2:1 leverage", - }, - "txid": [ - exchange_id - ] - } + + @property + def balance_request_mock_response_only_base(self): + return { + "makerCommission": 15, + "takerCommission": 15, + "buyerCommission": 0, + "sellerCommission": 0, + "canTrade": True, + "canWithdraw": True, + "canDeposit": True, + "updateTime": 123456789, + "accountType": "SPOT", + "balances": [{"asset": self.base_asset, "free": "10.0", "locked": "5.0"}], + "permissions": ["SPOT"], } - return order_placed - @aioresponses() - def test_get_asset_pairs(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) + @property + def balance_event_websocket_update(self): + return { + "c": "spot@private.account.v3.api", + "d": { + "a": self.base_asset, + "c": 1564034571105, + "f": "10", + "fd": "-4.990689704", + "l": "5", + "ld": "4.990689704", + "o": "ENTRUST_PLACE" + }, + "t": 1564034571073 + } + + @property + def expected_latest_price(self): + return 9999.9 - ret = self.async_run_with_timeout(self.exchange.get_asset_pairs()) + @property + def expected_supported_order_types(self): + return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] - self.assertIn(self.trading_pair, ret) - self.assertEqual( - ret[self.trading_pair], resp["result"][f"X{self.base_asset}{self.quote_asset}"] # shallow comparison is ok + @property + def expected_trading_rule(self): + return TradingRule( + trading_pair=self.trading_pair, + min_order_size=Decimal(self.trading_rules_request_mock_response["symbols"][0]["baseSizePrecision"]), + min_price_increment=Decimal( + f'1e-{self.trading_rules_request_mock_response["symbols"][0]["quotePrecision"]}'), + min_base_amount_increment=Decimal( + f'1e-{self.trading_rules_request_mock_response["symbols"][0]["baseAssetPrecision"]}'), + min_notional_size=Decimal(self.trading_rules_request_mock_response["symbols"][0]["quoteAmountPrecision"]), ) - @aioresponses() - def test_update_balances(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) + @property + def expected_logged_error_for_erroneous_trading_rule(self): + erroneous_rule = self.trading_rules_request_erroneous_mock_response["symbols"][0] + return f"Error parsing the trading pair rule {erroneous_rule}. Skipping." + + @property + def expected_exchange_order_id(self): + return 28 + + @property + def is_order_fill_http_update_included_in_status_update(self) -> bool: + return True + + @property + def is_order_fill_http_update_executed_during_websocket_order_event_processing(self) -> bool: + return False + + @property + def expected_partial_fill_price(self) -> Decimal: + return Decimal(10500) + + @property + def expected_partial_fill_amount(self) -> Decimal: + return Decimal("0.5") + + @property + def expected_fill_fee(self) -> TradeFeeBase: + return DeductedFromReturnsTradeFee( + percent_token=self.quote_asset, + flat_fees=[TokenAmount(token=self.quote_asset, amount=Decimal("30"))]) + + @property + def expected_fill_trade_id(self) -> str: + return str(30000) + + def exchange_symbol_for_tokens(self, base_token: str, quote_token: str) -> str: + return f"{base_token}{quote_token}" + + def create_exchange_instance(self): + client_config_map = ClientConfigAdapter(ClientConfigMap()) + return KrakenExchange( + client_config_map=client_config_map, + kraken_api_key="testAPIKey", + kraken_api_secret="testSecret", + trading_pairs=[self.trading_pair], + ) - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.BALANCE_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_balances_mock(base_asset_balance=10, quote_asset_balance=20) - mocked_api.post(regex_url, body=json.dumps(resp)) + def validate_auth_credentials_present(self, request_call: RequestCall): + self._validate_auth_credentials_taking_parameters_from_argument( + request_call_tuple=request_call, + params=request_call.kwargs["params"] or request_call.kwargs["data"] + ) - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" + def validate_order_creation_request(self, order: InFlightOrder, request_call: RequestCall): + request_data = dict(request_call.kwargs["data"]) + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_data["symbol"]) + self.assertEqual(order.trade_type.name.upper(), request_data["side"]) + self.assertEqual(KrakenExchange.kraken_order_type(OrderType.LIMIT), request_data["type"]) + self.assertEqual(Decimal("100"), Decimal(request_data["quantity"])) + self.assertEqual(Decimal("10000"), Decimal(request_data["price"])) + self.assertEqual(order.client_order_id, request_data["newClientOrderId"]) + + def validate_order_cancelation_request(self, order: InFlightOrder, request_call: RequestCall): + request_data = dict(request_call.kwargs["params"]) + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + request_data["symbol"]) + self.assertEqual(order.client_order_id, request_data["origClientOrderId"]) + + def validate_order_status_request(self, order: InFlightOrder, request_call: RequestCall): + request_params = request_call.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + request_params["symbol"]) + self.assertEqual(order.client_order_id, request_params["origClientOrderId"]) + + def validate_trades_request(self, order: InFlightOrder, request_call: RequestCall): + request_params = request_call.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + request_params["symbol"]) + self.assertEqual(order.exchange_order_id, str(request_params["orderId"])) + + def configure_successful_cancelation_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = self._order_cancelation_request_successful_mock_response(order=order) + mock_api.delete(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_erroneous_cancelation_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + mock_api.delete(regex_url, status=400, callback=callback) + return url + + def configure_order_not_found_error_cancelation_response( + self, order: InFlightOrder, mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None + ) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = {"code": -2011, "msg": "Unknown order sent."} + mock_api.delete(regex_url, status=400, body=json.dumps(response), callback=callback) + return url + + def configure_one_successful_one_erroneous_cancel_all_response( + self, + successful_order: InFlightOrder, + erroneous_order: InFlightOrder, + mock_api: aioresponses) -> List[str]: + """ + :return: a list of all configured URLs for the cancelations + """ + all_urls = [] + url = self.configure_successful_cancelation_response(order=successful_order, mock_api=mock_api) + all_urls.append(url) + url = self.configure_erroneous_cancelation_response(order=erroneous_order, mock_api=mock_api) + all_urls.append(url) + return all_urls + + def configure_completely_filled_order_status_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = self._order_status_request_completely_filled_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_canceled_order_status_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = self._order_status_request_canceled_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_erroneous_http_fill_trade_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + regex_url = re.compile(url + r"\?.*") + mock_api.get(regex_url, status=400, callback=callback) + return url + + def configure_open_order_status_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + """ + :return: the URL configured + """ + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp)) + response = self._order_status_request_open_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_http_error_order_status_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + mock_api.get(regex_url, status=401, callback=callback) + return url + + def configure_partially_filled_order_status_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = self._order_status_request_partially_filled_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_order_not_found_error_order_status_response( + self, order: InFlightOrder, mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None + ) -> List[str]: + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + response = {"code": -2013, "msg": "Order does not exist."} + mock_api.get(regex_url, body=json.dumps(response), status=400, callback=callback) + return [url] + + def configure_partial_fill_trade_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + regex_url = re.compile(url + r"\?.*") + response = self._order_fills_request_partial_fill_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def configure_full_fill_trade_response( + self, + order: InFlightOrder, + mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: + url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + regex_url = re.compile(url + r"\?.*") + response = self._order_fills_request_full_fill_mock_response(order=order) + mock_api.get(regex_url, body=json.dumps(response), callback=callback) + return url + + def order_event_for_new_order_websocket_update(self, order: InFlightOrder): + return { + "c": "spot@private.orders.v3.api", + "d": { + "A": 8.0, + "O": 1661938138000, + "S": 1, + "V": 10, + "a": 8, + "c": order.client_order_id, + "i": order.exchange_order_id, + "m": 0, + "o": 1, + "p": order.price, + "s": 1, + "v": order.amount, + "ap": 0, + "cv": 0, + "ca": 0 + }, + "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "t": 1499405658657 + } - self.async_run_with_timeout(self.exchange._update_balances()) + def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): + return { + "c": "spot@private.orders.v3.api", + "d": { + "A": 8.0, + "O": 1661938138000, + "S": 1, + "V": 10, + "a": 8, + "c": order.client_order_id, + "i": order.exchange_order_id, + "m": 0, + "o": 1, + "p": order.price, + "s": 4, + "v": order.amount, + "ap": 0, + "cv": 0, + "ca": 0 + }, + "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "t": 1499405658657 + } - self.assertEqual(self.exchange.available_balances[self.quote_asset], Decimal("18")) + def order_event_for_full_fill_websocket_update(self, order: InFlightOrder): + return { + "c": "spot@private.orders.v3.api", + "d": { + "A": 8.0, + "O": 1661938138000, + "S": 1, + "V": 10, + "a": 8, + "c": order.client_order_id, + "i": order.exchange_order_id, + "m": 0, + "o": 1, + "p": order.price, + "s": 2, + "v": order.amount, + "ap": 0, + "cv": 0, + "ca": 0 + }, + "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "t": 1499405658657 + } + + def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): + return { + "c": "spot@private.deals.v3.api", + "d": { + "p": order.price, + "v": order.amount, + "a": order.price * order.amount, + "S": 1, + "T": 1678901086198, + "t": "5bbb6ad8b4474570b155610e3960cd", + "c": order.client_order_id, + "i": order.exchange_order_id, + "m": 0, + "st": 0, + "n": Decimal(self.expected_fill_fee.flat_fees[0].amount), + "N": self.quote_asset + }, + "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "t": 1661938980285 + } @aioresponses() - def test_update_order_status_order_closed(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" + @patch("hummingbot.connector.time_synchronizer.TimeSynchronizer._current_seconds_counter") + def test_update_time_synchronizer_successfully(self, mock_api, seconds_counter_mock): + request_sent_event = asyncio.Event() + seconds_counter_mock.side_effect = [0, 0, 0] - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" + self.exchange._time_synchronizer.clear_time_offset_ms_samples() + url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") - mocked_api.post(regex_url, body=json.dumps(resp)) - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) - self.exchange.add_listener(MarketEvent.BuyOrderCompleted, self.event_listener) + response = {"serverTime": 1640000003000} - self.async_run_with_timeout(self.exchange._update_order_status()) + mock_api.get(regex_url, + body=json.dumps(response), + callback=lambda *args, **kwargs: request_sent_event.set()) - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCompletedEvent)) - self.assertNotIn(order_id, self.exchange.in_flight_orders) + self.async_run_with_timeout(self.exchange._update_time_synchronizer()) + + self.assertEqual(response["serverTime"] * 1e-3, self.exchange._time_synchronizer.time()) @aioresponses() - def test_check_network_success(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - resp = {"status": 200, "result": []} - mock_api.get(url, body=json.dumps(resp)) + def test_update_time_synchronizer_failure_is_logged(self, mock_api): + request_sent_event = asyncio.Event() + + url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + response = {"code": -1121, "msg": "Dummy error"} - ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + mock_api.get(regex_url, + body=json.dumps(response), + callback=lambda *args, **kwargs: request_sent_event.set()) - self.assertEqual(ret, NetworkStatus.CONNECTED) + self.async_run_with_timeout(self.exchange._update_time_synchronizer()) + + self.assertTrue(self.is_logged("NETWORK", "Error getting server time.")) @aioresponses() - def test_check_network_raises_cancelled_error(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - mock_api.get(url, exception=asyncio.CancelledError) + def test_update_time_synchronizer_raises_cancelled_error(self, mock_api): + url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - with self.assertRaises(asyncio.CancelledError): - self.async_run_with_timeout(coroutine=self.exchange.check_network()) + mock_api.get(regex_url, + exception=asyncio.CancelledError) + + self.assertRaises( + asyncio.CancelledError, + self.async_run_with_timeout, self.exchange._update_time_synchronizer()) @aioresponses() - def test_check_network_not_connected_for_error_status(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - resp = {"status": 405, "result": []} - mock_api.get(url, status=405, body=json.dumps(resp)) + def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + self.exchange.start_tracking_order( + order_id="OID1", + exchange_order_id="100234", + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + price=Decimal("10000"), + amount=Decimal("1"), + ) + order = self.exchange.in_flight_orders["OID1"] + + url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + trade_fill = { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "id": 28457, + "orderId": int(order.exchange_order_id), + "orderListId": -1, + "price": "9999", + "qty": "1", + "quoteQty": "48.000012", + "commission": "10.10000000", + "commissionAsset": self.quote_asset, + "time": 1499865549590, + "isBuyer": True, + "isMaker": False, + "isBestMatch": True + } + + trade_fill_non_tracked_order = { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "id": 30000, + "orderId": 99999, + "orderListId": -1, + "price": "4.00000100", + "qty": "12.00000000", + "quoteQty": "48.000012", + "commission": "10.10000000", + "commissionAsset": "BNB", + "time": 1499865549590, + "isBuyer": True, + "isMaker": False, + "isBestMatch": True + } - self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) + mock_response = [trade_fill, trade_fill_non_tracked_order] + mock_api.get(regex_url, body=json.dumps(mock_response)) + + self.exchange.add_exchange_order_ids_from_market_recorder( + {str(trade_fill_non_tracked_order["orderId"]): "OID99"}) + + self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) + + request = self._all_executed_requests(mock_api, url)[0] + self.validate_auth_credentials_present(request) + request_params = request.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) + + 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(Decimal(trade_fill["price"]), fill_event.price) + self.assertEqual(Decimal(trade_fill["qty"]), fill_event.amount) + self.assertEqual(0.0, fill_event.trade_fee.percent) + self.assertEqual([TokenAmount(trade_fill["commissionAsset"], Decimal(trade_fill["commission"]))], + fill_event.trade_fee.flat_fees) + + fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1] + self.assertEqual(float(trade_fill_non_tracked_order["time"]) * 1e-3, fill_event.timestamp) + self.assertEqual("OID99", fill_event.order_id) + self.assertEqual(self.trading_pair, fill_event.trading_pair) + self.assertEqual(TradeType.BUY, fill_event.trade_type) + self.assertEqual(OrderType.LIMIT, fill_event.order_type) + self.assertEqual(Decimal(trade_fill_non_tracked_order["price"]), fill_event.price) + self.assertEqual(Decimal(trade_fill_non_tracked_order["qty"]), fill_event.amount) + self.assertEqual(0.0, fill_event.trade_fee.percent) + self.assertEqual([ + TokenAmount( + trade_fill_non_tracked_order["commissionAsset"], + Decimal(trade_fill_non_tracked_order["commission"]))], + fill_event.trade_fee.flat_fees) + self.assertTrue(self.is_logged( + "INFO", + f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" + )) @aioresponses() - def test_get_open_orders_with_userref(self, mocked_api): - sent_messages = [] - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" + def test_update_order_fills_request_parameters(self, mock_api): + self.exchange._set_current_timestamp(0) + self.exchange._last_poll_timestamp = -1 + + url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) - userref = 1 - ret = self.async_run_with_timeout(self.exchange.get_open_orders_with_userref(userref)) + mock_response = [] + mock_api.get(regex_url, body=json.dumps(mock_response)) + + self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - self.assertEqual(len(sent_messages), 1) + request = self._all_executed_requests(mock_api, url)[0] + self.validate_auth_credentials_present(request) + request_params = request.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) + self.assertNotIn("startTime", request_params) - sent_message = sent_messages[0][1]["data"] + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) + self.exchange._last_trades_poll_kraken_timestamp = 10 + self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - self.assertEqual(sent_message["userref"], userref) - self.assertEqual(ret, resp["result"]) # shallow comparison ok + request = self._all_executed_requests(mock_api, url)[1] + self.validate_auth_credentials_present(request) + request_params = request.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) + self.assertEqual(10 * 1e3, request_params["startTime"]) @aioresponses() - def test_get_order(self, mocked_api): - sent_messages = [] - order_id = "someId" - exchange_id = "someExchangeId" + def test_update_order_fills_from_trades_with_repeated_fill_triggers_only_one_event(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" + url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") - mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) + + trade_fill_non_tracked_order = { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "id": 30000, + "orderId": 99999, + "orderListId": -1, + "price": "4.00000100", + "qty": "12.00000000", + "quoteQty": "48.000012", + "commission": "10.10000000", + "commissionAsset": "BNB", + "time": 1499865549590, + "isBuyer": True, + "isMaker": False, + "isBestMatch": True + } + + mock_response = [trade_fill_non_tracked_order, trade_fill_non_tracked_order] + mock_api.get(regex_url, body=json.dumps(mock_response)) + + self.exchange.add_exchange_order_ids_from_market_recorder( + {str(trade_fill_non_tracked_order["orderId"]): "OID99"}) + + self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) + + request = self._all_executed_requests(mock_api, url)[0] + self.validate_auth_credentials_present(request) + request_params = request.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) + + self.assertEqual(1, len(self.order_filled_logger.event_log)) + fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] + self.assertEqual(float(trade_fill_non_tracked_order["time"]) * 1e-3, fill_event.timestamp) + self.assertEqual("OID99", fill_event.order_id) + self.assertEqual(self.trading_pair, fill_event.trading_pair) + self.assertEqual(TradeType.BUY, fill_event.trade_type) + self.assertEqual(OrderType.LIMIT, fill_event.order_type) + self.assertEqual(Decimal(trade_fill_non_tracked_order["price"]), fill_event.price) + self.assertEqual(Decimal(trade_fill_non_tracked_order["qty"]), fill_event.amount) + self.assertEqual(0.0, fill_event.trade_fee.percent) + self.assertEqual([ + TokenAmount(trade_fill_non_tracked_order["commissionAsset"], + Decimal(trade_fill_non_tracked_order["commission"]))], + fill_event.trade_fee.flat_fees) + self.assertTrue(self.is_logged( + "INFO", + f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" + )) + + @aioresponses() + def test_update_order_status_when_failed(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, + order_id="OID1", + exchange_order_id="100234", trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, order_type=OrderType.LIMIT, - userref=1, + trade_type=TradeType.BUY, + price=Decimal("10000"), + amount=Decimal("1"), ) - ret = self.async_run_with_timeout(self.exchange.get_order(client_order_id=order_id)) + order = self.exchange.in_flight_orders["OID1"] - self.assertEqual(len(sent_messages), 1) + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - sent_message = sent_messages[0][1]["data"] + order_status = { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": int(order.exchange_order_id), + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": "10000.0", + "origQty": "1.0", + "executedQty": "0.0", + "cummulativeQuoteQty": "0.0", + "status": "REJECTED", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": True, + "origQuoteOrderQty": "10000.000000" + } - self.assertEqual(sent_message["txid"], exchange_id) - self.assertEqual(ret, resp["result"]) # shallow comparison ok + mock_response = order_status + mock_api.get(regex_url, body=json.dumps(mock_response)) - @aioresponses() - def test_execute_buy(self, mocked_api): - self.exchange.start(self.clock, self.start_time) - self.simulate_trading_rules_initialized(mocked_api) + self.async_run_with_timeout(self.exchange._update_order_status()) - order_id = "someId" - exchange_id = "someExchangeId" - userref = 1 - quantity = 1 - price = 2 + request = self._all_executed_requests(mock_api, url)[0] + self.validate_auth_credentials_present(request) + request_params = request.kwargs["params"] + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) + self.assertEqual(order.client_order_id, request_params["origClientOrderId"]) + + failure_event: MarketOrderFailureEvent = self.order_failure_logger.event_log[0] + self.assertEqual(self.exchange.current_timestamp, failure_event.timestamp) + self.assertEqual(order.client_order_id, failure_event.order_id) + self.assertEqual(order.order_type, failure_event.order_type) + self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) + self.assertTrue( + self.is_logged( + "INFO", + f"Order {order.client_order_id} has failed. Order Update: OrderUpdate(trading_pair='{self.trading_pair}'," + f" update_timestamp={order_status['updateTime'] * 1e-3}, new_state={repr(OrderState.FAILED)}, " + f"client_order_id='{order.client_order_id}', exchange_order_id='{order.exchange_order_id}', " + "misc_updates=None)") + ) - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.add_listener(MarketEvent.BuyOrderCreated, self.event_listener) - self.async_run_with_timeout( - self.exchange.execute_buy( - order_id, - self.trading_pair, - amount=Decimal(quantity), - order_type=OrderType.LIMIT, - price=Decimal(price), - userref=userref, - ) + @patch("hummingbot.connector.utils.get_tracking_nonce") + def test_client_order_id_on_order(self, mocked_nonce): + mocked_nonce.return_value = 7 + + result = self.exchange.buy( + trading_pair=self.trading_pair, + amount=Decimal("1"), + order_type=OrderType.LIMIT, + price=Decimal("2"), + ) + expected_client_order_id = get_new_client_order_id( + is_buy=True, + trading_pair=self.trading_pair, + hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID_PREFIX, + max_id_len=CONSTANTS.MAX_ORDER_ID_LEN, ) - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCreatedEvent)) - self.assertIn(order_id, self.exchange.in_flight_orders) + self.assertEqual(result, expected_client_order_id) - @aioresponses() - def test_execute_sell(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" - userref = 1 - quantity = 1 - price = 2 - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="sell") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.start(self.clock, self.start_time) - self.simulate_trading_rules_initialized(mocked_api) - self.exchange.add_listener(MarketEvent.SellOrderCreated, self.event_listener) - self.async_run_with_timeout( - self.exchange.execute_sell( - order_id, - self.trading_pair, - amount=Decimal(quantity), - order_type=OrderType.LIMIT, - price=Decimal(price), - userref=userref, - ) + result = self.exchange.sell( + trading_pair=self.trading_pair, + amount=Decimal("1"), + order_type=OrderType.LIMIT, + price=Decimal("2"), + ) + expected_client_order_id = get_new_client_order_id( + is_buy=False, + trading_pair=self.trading_pair, + hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID_PREFIX, + max_id_len=CONSTANTS.MAX_ORDER_ID_LEN, ) - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], SellOrderCreatedEvent)) - self.assertIn(order_id, self.exchange.in_flight_orders) + self.assertEqual(result, expected_client_order_id) - @aioresponses() - def test_execute_cancel(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" + def test_time_synchronizer_related_request_error_detection(self): + exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " + "Error: {'code':-1021,'msg':'Timestamp for this request is outside of the recvWindow.'}") + self.assertTrue(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.CANCEL_ORDER_PATH_URL}" + exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " + "Error: {'code':-1021,'msg':'Timestamp for this request was 1000ms ahead of the server's " + "time.'}") + self.assertTrue(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) + + exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " + "Error: {'code':-1022,'msg':'Timestamp for this request was 1000ms ahead of the server's " + "time.'}") + self.assertFalse(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) + + exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " + "Error: {'code':-1021,'msg':'Other error.'}") + self.assertFalse(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) + + @aioresponses() + def test_place_order_manage_server_overloaded_error_unkown_order(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = { - "error": [], - "result": { - "count": 1 - } - } - mocked_api.post(regex_url, body=json.dumps(resp)) + mock_response = {"code": -1003, "msg": "Unknown error, please check your request or try again later."} + mock_api.post(regex_url, body=json.dumps(mock_response), status=503) - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, + o_id, transact_time = self.async_run_with_timeout(self.exchange._place_order( + order_id="test_order_id", trading_pair=self.trading_pair, + amount=Decimal("1"), trade_type=TradeType.BUY, - price=2, - amount=1, order_type=OrderType.LIMIT, - userref=1, - ) - self.exchange.in_flight_orders[order_id].update_exchange_order_id(exchange_id) - self.exchange.in_flight_orders[order_id].last_state = "pending" - self.exchange.add_listener(MarketEvent.OrderCancelled, self.event_listener) - ret = self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) + price=Decimal("2"), + )) + self.assertEqual(o_id, "UNKNOWN") + + @aioresponses() + def test_place_order_manage_server_overloaded_error_failure(self, mock_api): + self.exchange._set_current_timestamp(1640780000) + self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - + self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) + + url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + mock_response = {"code": -1003, "msg": "Service Unavailable."} + mock_api.post(regex_url, body=json.dumps(mock_response), status=503) + + self.assertRaises( + IOError, + self.async_run_with_timeout, + self.exchange._place_order( + order_id="test_order_id", + trading_pair=self.trading_pair, + amount=Decimal("1"), + trade_type=TradeType.BUY, + order_type=OrderType.LIMIT, + price=Decimal("2"), + )) + + mock_response = {"code": -1003, "msg": "Internal error; unable to process your request. Please try again."} + mock_api.post(regex_url, body=json.dumps(mock_response), status=503) + + self.assertRaises( + IOError, + self.async_run_with_timeout, + self.exchange._place_order( + order_id="test_order_id", + trading_pair=self.trading_pair, + amount=Decimal("1"), + trade_type=TradeType.BUY, + order_type=OrderType.LIMIT, + price=Decimal("2"), + )) + + def test_format_trading_rules__min_notional_present(self): + trading_rules = [{ + "symbol": "COINALPHAHBOT", + "baseSizePrecision": 1e-8, + "quotePrecision": 8, + "baseAssetPrecision": 8, + "status": "ENABLED", + "quoteAmountPrecision": "0.001", + "orderTypes": ["LIMIT", "MARKET"], + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + }, { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + }, { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00300000" + } + ], + "permissions": [ + "SPOT" + ] + }] + exchange_info = {"symbols": trading_rules} + + result = self.async_run_with_timeout(self.exchange._format_trading_rules(exchange_info)) + + self.assertEqual(result[0].min_notional_size, Decimal("0.00100000")) + + def _validate_auth_credentials_taking_parameters_from_argument(self, + request_call_tuple: RequestCall, + params: Dict[str, Any]): + self.assertIn("timestamp", params) + self.assertIn("signature", params) + request_headers = request_call_tuple.kwargs["headers"] + self.assertIn("X-MEXC-APIKEY", request_headers) + self.assertEqual("testAPIKey", request_headers["X-MEXC-APIKEY"]) + + def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "origClientOrderId": order.exchange_order_id or "dummyOrdId", + "orderId": 4, + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": str(order.price), + "origQty": str(order.amount), + "executedQty": str(Decimal("0")), + "cummulativeQuoteQty": str(Decimal("0")), + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY" + } - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], OrderCancelledEvent)) - self.assertNotIn(order_id, self.exchange.in_flight_orders) - self.assertEqual(ret["origClientOrderId"], order_id) + def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": order.exchange_order_id, + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": str(order.price), + "origQty": str(order.amount), + "executedQty": str(order.amount), + "cummulativeQuoteQty": str(order.price + Decimal(2)), + "status": "FILLED", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": True, + "origQuoteOrderQty": str(order.price * order.amount) + } - def test_execute_cancel_ignores_local_orders(self): - order_id = "someId" - exchange_id = "someExchangeId" + def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": order.exchange_order_id, + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": str(order.price), + "origQty": str(order.amount), + "executedQty": "0.0", + "cummulativeQuoteQty": "10000.0", + "status": "CANCELED", + "timeInForce": "GTC", + "type": order.order_type.name.upper(), + "side": order.trade_type.name.upper(), + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": True, + "origQuoteOrderQty": str(order.price * order.amount) + } - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) + def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": order.exchange_order_id, + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": str(order.price), + "origQty": str(order.amount), + "executedQty": "0.0", + "cummulativeQuoteQty": "10000.0", + "status": "NEW", + "timeInForce": "GTC", + "type": order.order_type.name.upper(), + "side": order.trade_type.name.upper(), + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": True, + "origQuoteOrderQty": str(order.price * order.amount) + } - with self.assertRaises(KrakenInFlightOrderNotCreated): - self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) + def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: + return { + "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "orderId": order.exchange_order_id, + "orderListId": -1, + "clientOrderId": order.client_order_id, + "price": str(order.price), + "origQty": str(order.amount), + "executedQty": str(order.amount), + "cummulativeQuoteQty": str(self.expected_partial_fill_amount * order.price), + "status": "PARTIALLY_FILLED", + "timeInForce": "GTC", + "type": order.order_type.name.upper(), + "side": order.trade_type.name.upper(), + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559, + "updateTime": 1499827319559, + "isWorking": True, + "origQuoteOrderQty": str(order.price * order.amount) + } + + def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): + return [ + { + "symbol": self.exchange_symbol_for_tokens(order.base_asset, order.quote_asset), + "id": self.expected_fill_trade_id, + "orderId": int(order.exchange_order_id), + "orderListId": -1, + "price": str(self.expected_partial_fill_price), + "qty": str(self.expected_partial_fill_amount), + "quoteQty": str(self.expected_partial_fill_amount * self.expected_partial_fill_price), + "commission": str(self.expected_fill_fee.flat_fees[0].amount), + "commissionAsset": self.expected_fill_fee.flat_fees[0].token, + "time": 1499865549590, + "isBuyer": True, + "isMaker": False, + "isBestMatch": True + } + ] + + def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): + return [ + { + "symbol": self.exchange_symbol_for_tokens(order.base_asset, order.quote_asset), + "id": self.expected_fill_trade_id, + "orderId": int(order.exchange_order_id), + "orderListId": -1, + "price": str(order.price), + "qty": str(order.amount), + "quoteQty": str(order.amount * order.price), + "commission": str(self.expected_fill_fee.flat_fees[0].amount), + "commissionAsset": self.expected_fill_fee.flat_fees[0].token, + "time": 1499865549590, + "isBuyer": True, + "isMaker": False, + "isBestMatch": True + } + ] diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py index 6de2297f5f..ed921aec7c 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py @@ -1,8 +1,9 @@ from decimal import Decimal from unittest import TestCase -from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrder -from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder +from hummingbot.core.data_type.common import OrderType, TradeType, PositionAction +from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate class KrakenInFlightOrderTests(TestCase): @@ -19,7 +20,7 @@ def test_order_is_local_after_creation(self): userref=1, ) - self.assertTrue(order.is_local) + self.assertTrue(order.is_pending_create) def test_serialize_order_to_json(self): order = KrakenInFlightOrder( @@ -32,7 +33,7 @@ def test_serialize_order_to_json(self): amount=Decimal(1), creation_timestamp=1640001112.0, userref=2, - initial_state="OPEN", + initial_state=OrderState.OPEN, ) expected_json = { @@ -43,13 +44,15 @@ def test_serialize_order_to_json(self): "trade_type": order.trade_type.name, "price": str(order.price), "amount": str(order.amount), - "last_state": order.last_state, "executed_amount_base": str(order.executed_amount_base), "executed_amount_quote": str(order.executed_amount_quote), - "fee_asset": order.fee_asset, - "fee_paid": str(order.fee_paid), + "last_state": OrderState.OPEN, + "leverage": 1, + "position": PositionAction.NIL, + "userref": 2, "creation_timestamp": 1640001112.0, - "userref": order.userref, + "last_update_timestamp": 1640001112.0, + "order_fills": {} } self.assertEqual(expected_json, order.to_json()) @@ -83,8 +86,5 @@ def test_deserialize_order_from_json(self): self.assertEqual(Decimal(json["amount"]), order.amount) self.assertEqual(Decimal(json["executed_amount_base"]), order.executed_amount_base) self.assertEqual(Decimal(json["executed_amount_quote"]), order.executed_amount_quote) - self.assertEqual(json["fee_asset"], order.fee_asset) - self.assertEqual(Decimal(json["fee_paid"]), order.fee_paid) - self.assertEqual(json["last_state"], order.last_state) self.assertEqual(json["creation_timestamp"], order.creation_timestamp) self.assertEqual(json["userref"], order.userref) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py new file mode 100644 index 0000000000..3f2a1c8719 --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py @@ -0,0 +1,96 @@ +from unittest import TestCase + +from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook +from hummingbot.core.data_type.order_book_message import OrderBookMessageType + + +class KrakenOrderBookTests(TestCase): + + def test_snapshot_message_from_exchange(self): + snapshot_message = KrakenOrderBook.snapshot_message_from_exchange( + msg={ + "lastUpdateId": 1, + "bids": [ + ["4.00000000", "431.00000000"] + ], + "asks": [ + ["4.00000200", "12.00000000"] + ] + }, + timestamp=1640000000.0, + metadata={"trading_pair": "COINALPHA/HBOT"} + ) + + self.assertEqual("COINALPHA-HBOT", snapshot_message.trading_pair) + self.assertEqual(OrderBookMessageType.SNAPSHOT, snapshot_message.type) + self.assertEqual(1640000000.0, snapshot_message.timestamp) + self.assertEqual(1, snapshot_message.update_id) + self.assertEqual(-1, snapshot_message.trade_id) + self.assertEqual(1, len(snapshot_message.bids)) + self.assertEqual(4.0, snapshot_message.bids[0].price) + self.assertEqual(431.0, snapshot_message.bids[0].amount) + self.assertEqual(1, snapshot_message.bids[0].update_id) + self.assertEqual(1, len(snapshot_message.asks)) + self.assertEqual(4.000002, snapshot_message.asks[0].price) + self.assertEqual(12.0, snapshot_message.asks[0].amount) + self.assertEqual(1, snapshot_message.asks[0].update_id) + + def test_diff_message_from_exchange(self): + diff_msg = KrakenOrderBook.diff_message_from_exchange( + msg={ + "trading_pair": "COINALPHA-HBOT", + "asks": [ + [ + "5541.30000", + "2.50700000", + "1534614248.123678" + ], + ], + "bids": [ + [ + "5541.20000", + "1.52900000", + "1534614248.765567" + ], + ] + }, + timestamp=1640000000000, + ) + + self.assertEqual("COINALPHA-HBOT", diff_msg.trading_pair) + self.assertEqual(1640000000.0, diff_msg.timestamp) + self.assertEqual(3407459756, diff_msg.update_id) + self.assertEqual(OrderBookMessageType.DIFF, diff_msg.type) + self.assertEqual(-1, diff_msg.trade_id) + self.assertEqual(1, len(diff_msg.bids)) + self.assertEqual(5541.2, diff_msg.bids[0].price) + self.assertEqual(1.529, diff_msg.bids[0].amount) + self.assertEqual(1534614248.765567, diff_msg.bids[0].update_id) + self.assertEqual(1, len(diff_msg.asks)) + self.assertEqual(5541.3, diff_msg.asks[0].price) + self.assertEqual(2.507, diff_msg.asks[0].amount) + self.assertEqual(1534614248.765567, diff_msg.asks[0].update_id) + + def test_trade_message_from_exchange(self): + trade_update = { + "pair": "COINALPHA-HBOT", + "trade": [ + "5541.20000", + "0.15850568", + "1534614057.321597", + "s", + "l", + "" + ] + } + + trade_message = KrakenOrderBook.trade_message_from_exchange( + msg=trade_update, + metadata={"trading_pair": "COINALPHA-HBOT"}, + ) + + self.assertEqual("COINALPHA-HBOT", trade_message.trading_pair) + self.assertEqual(OrderBookMessageType.TRADE, trade_message.type) + self.assertEqual(1534614057.321597, trade_message.timestamp) + self.assertEqual(1534614057.321597, trade_message.update_id) + self.assertEqual(1534614057321.597, trade_message.trade_id) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py new file mode 100644 index 0000000000..10851be408 --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py @@ -0,0 +1,41 @@ +import unittest + +from hummingbot.connector.exchange.kraken import kraken_utils as utils + + +class KrakenUtilTestCases(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.base_asset = "XBT" + cls.hb_base_asset = "BTC" + cls.quote_asset = "USDT" + cls.trading_pair = f"{cls.hb_base_asset}-{cls.quote_asset}" + cls.hb_trading_pair = f"{cls.hb_base_asset}-{cls.quote_asset}" + cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}" + cls.ex_ws_trading_pair = f"{cls.base_asset}/{cls.quote_asset}" + + def test_convert_from_exchange_symbol(self): + self.assertEqual(self.hb_base_asset, utils.convert_from_exchange_symbol(self.base_asset)) + self.assertEqual(self.quote_asset, utils.convert_from_exchange_symbol(self.quote_asset)) + + def test_convert_to_exchange_symbol(self): + self.assertEqual(self.base_asset, utils.convert_to_exchange_symbol(self.hb_base_asset)) + self.assertEqual(self.quote_asset, utils.convert_to_exchange_symbol(self.quote_asset)) + + def test_convert_to_exchange_trading_pair(self): + self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.hb_trading_pair)) + self.assertEqual(self.ex_ws_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_ws_trading_pair)) + self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_trading_pair)) + + def test_split_to_base_quote(self): + self.assertEqual((self.hb_base_asset,self.quote_asset), utils.split_to_base_quote(self.trading_pair)) + + def test_convert_from_exchange_trading_pair(self): + self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair)) + self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair,("XBTUSDT","ETHUSDT"))) + self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_ws_trading_pair)) + + def test_build_rate_limits_by_tier(self): + self.assertIsNotNone(utils.build_rate_limits_by_tier()) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py b/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py new file mode 100644 index 0000000000..a8d120fe49 --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py @@ -0,0 +1,43 @@ +import unittest + +import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken import kraken_web_utils as web_utils + + +class KrakenUtilTestCases(unittest.TestCase): + + def test_public_rest_url(self): + path_url = "/TEST_PATH" + expected_url = CONSTANTS.BASE_URL + path_url + self.assertEqual(expected_url, web_utils.public_rest_url(path_url)) + + def test_private_rest_url(self): + path_url = "/TEST_PATH" + expected_url = CONSTANTS.BASE_URL + path_url + self.assertEqual(expected_url, web_utils.private_rest_url(path_url)) + + def test_is_exchange_information_valid(self): + invalid_info_1 = { + "XBTUSDT": { + "altname": "XBTUSDT.d", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + } + } + + self.assertFalse(web_utils.is_exchange_information_valid(invalid_info_1)) + valid_info_1 = { + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + } + } + + self.assertTrue(web_utils.is_exchange_information_valid(valid_info_1)) diff --git a/test/hummingbot/connector/exchange/kraken_v1/__init__.py b/test/hummingbot/connector/exchange/kraken_v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py new file mode 100644 index 0000000000..55538894aa --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py @@ -0,0 +1,271 @@ +import asyncio +import json +import re +import unittest +from decimal import Decimal +from typing import Awaitable, Dict, List +from unittest.mock import AsyncMock, patch + +from aioresponses import aioresponses + +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource +from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier +from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler +from hummingbot.core.data_type.order_book import OrderBook, OrderBookMessage + + +class KrakenAPIOrderBookDataSourceTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.ev_loop = asyncio.get_event_loop() + cls.base_asset = "COINALPHA" + cls.quote_asset = "HBOT" + cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + cls.api_tier = KrakenAPITier.STARTER + + def setUp(self) -> None: + super().setUp() + self.mocking_assistant = NetworkMockingAssistant() + self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) + self.data_source = KrakenAPIOrderBookDataSource(self.throttler, trading_pairs=[self.trading_pair]) + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + def get_last_traded_prices_mock(self, last_trade_close: Decimal) -> Dict: + last_traded_prices = { + "error": [], + "result": { + f"X{self.base_asset}{self.quote_asset}": { + "a": [ + "52609.60000", + "1", + "1.000" + ], + "b": [ + "52609.50000", + "1", + "1.000" + ], + "c": [ + str(last_trade_close), + "0.00080000" + ], + "v": [ + "1920.83610601", + "7954.00219674" + ], + "p": [ + "52389.94668", + "54022.90683" + ], + "t": [ + 23329, + 80463 + ], + "l": [ + "51513.90000", + "51513.90000" + ], + "h": [ + "53219.90000", + "57200.00000" + ], + "o": "52280.40000" + } + } + } + return last_traded_prices + + def get_depth_mock(self) -> Dict: + depth = { + "error": [], + "result": { + f"X{self.base_asset}{self.quote_asset}": { + "asks": [ + [ + "52523.00000", + "1.199", + 1616663113 + ], + [ + "52536.00000", + "0.300", + 1616663112 + ] + ], + "bids": [ + [ + "52522.90000", + "0.753", + 1616663112 + ], + [ + "52522.80000", + "0.006", + 1616663109 + ] + ] + } + } + } + return depth + + def get_public_asset_pair_mock(self) -> Dict: + asset_pairs = { + "error": [], + "result": { + f"X{self.base_asset}{self.quote_asset}": { + "altname": f"{self.base_asset}{self.quote_asset}", + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 5, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [ + 2, + 3, + 4, + 5 + ], + "leverage_sell": [ + 2, + 3, + 4, + 5 + ], + "fees": [ + [ + 0, + 0.26 + ], + [ + 50000, + 0.24 + ], + ], + "fees_maker": [ + [ + 0, + 0.16 + ], + [ + 50000, + 0.14 + ], + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.005" + }, + } + } + return asset_pairs + + def get_trade_data_mock(self) -> List: + trade_data = [ + 0, + [ + [ + "5541.20000", + "0.15850568", + "1534614057.321597", + "s", + "l", + "" + ], + [ + "6060.00000", + "0.02455000", + "1534614057.324998", + "b", + "l", + "" + ] + ], + "trade", + f"{self.base_asset}/{self.quote_asset}" + ] + return trade_data + + @aioresponses() + def test_get_last_traded_prices(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + last_traded_price = Decimal("52641.10000") + resp = self.get_last_traded_prices_mock(last_trade_close=last_traded_price) + mocked_api.get(regex_url, body=json.dumps(resp)) + + ret = self.async_run_with_timeout( + KrakenAPIOrderBookDataSource.get_last_traded_prices( + trading_pairs=[self.trading_pair], throttler=self.throttler + ) + ) + + self.assertIn(self.trading_pair, ret) + self.assertEqual(float(last_traded_price), ret[self.trading_pair]) + + @aioresponses() + def test_get_new_order_book(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_depth_mock() + mocked_api.get(regex_url, body=json.dumps(resp)) + + ret = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair)) + + self.assertTrue(isinstance(ret, OrderBook)) + + bids_df, asks_df = ret.snapshot + pair_data = resp["result"][f"X{self.base_asset}{self.quote_asset}"] + first_bid_price = float(pair_data["bids"][0][0]) + first_ask_price = float(pair_data["asks"][0][0]) + + self.assertEqual(first_bid_price, bids_df.iloc[0]["price"]) + self.assertEqual(first_ask_price, asks_df.iloc[0]["price"]) + + # @aioresponses() + # def test_fetch_trading_pairs(self, mocked_api): + # url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + # regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + # resp = self.get_public_asset_pair_mock() + # mocked_api.get(regex_url, body=json.dumps(resp)) + # + # resp = self.async_run_with_timeout(KrakenAPIOrderBookDataSource.fetch_trading_pairs(), 2) + # + # self.assertTrue(len(resp) == 1) + # self.assertIn(self.trading_pair, resp) + + @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) + def test_listen_for_trades(self, ws_connect_mock): + ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() + resp = self.get_trade_data_mock() + self.mocking_assistant.add_websocket_aiohttp_message( + websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) + ) + output_queue = asyncio.Queue() + + self.ev_loop.create_task(self.data_source.listen_for_trades(self.ev_loop, output_queue)) + self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value) + + self.assertTrue(not output_queue.empty()) + msg = output_queue.get_nowait() + self.assertTrue(isinstance(msg, OrderBookMessage)) + first_trade_price = resp[1][0][0] + self.assertEqual(msg.content["price"], first_trade_price) + + self.assertTrue(not output_queue.empty()) + msg = output_queue.get_nowait() + self.assertTrue(isinstance(msg, OrderBookMessage)) + second_trade_price = resp[1][1][0] + self.assertEqual(msg.content["price"], second_trade_price) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py new file mode 100644 index 0000000000..7786f2ffa6 --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py @@ -0,0 +1,138 @@ +import asyncio +import json +import re +import unittest +from typing import Awaitable, Dict, List +from unittest.mock import AsyncMock, patch + +from aioresponses import aioresponses + +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource +from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier +from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant +from hummingbot.core.api_throttler.async_throttler import AsyncThrottler + + +class KrakenAPIUserStreamDataSourceTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.ev_loop = asyncio.get_event_loop() + cls.base_asset = "COINALPHA" + cls.quote_asset = "HBOT" + cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + cls.api_tier = KrakenAPITier.STARTER + + def setUp(self) -> None: + super().setUp() + self.mocking_assistant = NetworkMockingAssistant() + self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) + not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" + kraken_auth = KrakenAuth(api_key="someKey", secret_key=not_a_real_secret) + self.data_source = KrakenAPIUserStreamDataSource(self.throttler, kraken_auth) + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + @staticmethod + def get_auth_response_mock() -> Dict: + auth_resp = { + "error": [], + "result": { + "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", + "expires": 900 + } + } + return auth_resp + + @staticmethod + def get_open_orders_mock() -> List: + open_orders = [ + [ + { + "OGTT3Y-C6I3P-XRI6HX": { + "status": "closed" + } + }, + { + "OGTT3Y-C6I3P-XRI6HX": { + "status": "closed" + } + } + ], + "openOrders", + { + "sequence": 59342 + } + ] + return open_orders + + @staticmethod + def get_own_trades_mock() -> List: + own_trades = [ + [ + { + "TDLH43-DVQXD-2KHVYY": { + "cost": "1000000.00000", + "fee": "1600.00000", + "margin": "0.00000", + "ordertxid": "TDLH43-DVQXD-2KHVYY", + "ordertype": "limit", + "pair": "XBT/EUR", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": "100000.00000", + "time": "1560516023.070651", + "type": "sell", + "vol": "1000000000.00000000" + } + }, + ], + "ownTrades", + { + "sequence": 2948 + } + ] + return own_trades + + @aioresponses() + def test_get_auth_token(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_auth_response_mock() + mocked_api.post(regex_url, body=json.dumps(resp)) + + ret = self.async_run_with_timeout(self.data_source.get_auth_token()) + + self.assertEqual(ret, resp["result"]["token"]) + + @aioresponses() + @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) + def test_listen_for_user_stream(self, mocked_api, ws_connect_mock): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_auth_response_mock() + mocked_api.post(regex_url, body=json.dumps(resp)) + + ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() + output_queue = asyncio.Queue() + self.ev_loop.create_task(self.data_source.listen_for_user_stream(output_queue)) + + resp = self.get_open_orders_mock() + self.mocking_assistant.add_websocket_aiohttp_message( + websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) + ) + ret = self.async_run_with_timeout(coroutine=output_queue.get()) + + self.assertEqual(ret, resp) + + resp = self.get_own_trades_mock() + self.mocking_assistant.add_websocket_aiohttp_message( + websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) + ) + ret = self.async_run_with_timeout(coroutine=output_queue.get()) + + self.assertEqual(ret, resp) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py new file mode 100644 index 0000000000..6096c57f47 --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py @@ -0,0 +1,454 @@ +import asyncio +import json +import re +import unittest +from decimal import Decimal +from functools import partial +from typing import Awaitable, Dict + +from aioresponses import aioresponses + +from hummingbot.client.config.client_config_map import ClientConfigMap +from hummingbot.client.config.config_helpers import ClientConfigAdapter +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange +from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrderNotCreated +from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant +from hummingbot.core.clock import Clock, ClockMode +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.event.event_logger import EventLogger +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketEvent, + OrderCancelledEvent, + SellOrderCreatedEvent, +) +from hummingbot.core.network_iterator import NetworkStatus + + +class KrakenExchangeTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.ev_loop = asyncio.get_event_loop() + cls.base_asset = "COINALPHA" + cls.quote_asset = "HBOT" + cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + + def setUp(self) -> None: + super().setUp() + self.mocking_assistant = NetworkMockingAssistant() + self.event_listener = EventLogger() + not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" + self.client_config_map = ClientConfigAdapter(ClientConfigMap()) + + self.exchange = KrakenExchange( + client_config_map=self.client_config_map, + kraken_api_key="someKey", + kraken_secret_key=not_a_real_secret, + trading_pairs=[self.trading_pair], + ) + self.start_time = 1 + self.clock = Clock(clock_mode=ClockMode.BACKTEST, start_time=self.start_time) + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + def simulate_trading_rules_initialized(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + resp = self.get_asset_pairs_mock() + mocked_api.get(url, body=json.dumps(resp)) + + self.async_run_with_timeout(self.exchange._update_trading_rules(), timeout=2) + + @staticmethod + def register_sent_request(requests_list, url, **kwargs): + requests_list.append((url, kwargs)) + + def get_asset_pairs_mock(self) -> Dict: + asset_pairs = { + "error": [], + "result": { + f"X{self.base_asset}{self.quote_asset}": { + "altname": f"{self.base_asset}{self.quote_asset}", + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": f"{self.base_asset}", + "aclass_quote": "currency", + "quote": f"{self.quote_asset}", + "lot": "unit", + "pair_decimals": 5, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [ + 2, + 3, + ], + "leverage_sell": [ + 2, + 3, + ], + "fees": [ + [ + 0, + 0.26 + ], + [ + 50000, + 0.24 + ], + ], + "fees_maker": [ + [ + 0, + 0.16 + ], + [ + 50000, + 0.14 + ], + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.005" + }, + } + } + return asset_pairs + + def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: + balances = { + "error": [], + "result": { + self.base_asset: str(base_asset_balance), + self.quote_asset: str(quote_asset_balance), + "USDT": "171288.6158", + } + } + return balances + + def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: + open_orders = { + "error": [], + "result": { + "open": { + "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), + } + } + } + return open_orders + + def get_query_orders_mock( + self, exchange_id: str, quantity: float, price: float, order_type: str, status: str + ) -> Dict: + query_orders = { + "error": [], + "result": { + exchange_id: self.get_order_status_mock(quantity, price, order_type, status) + } + } + return query_orders + + def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: + order_status = { + "refid": None, + "userref": 0, + "status": status, + "opentm": 1616666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": f"{self.base_asset}{self.quote_asset}", + "type": order_type, + "ordertype": "limit", + "price": str(price), + "price2": "0", + "leverage": "none", + "order": f"buy {quantity} {self.base_asset}{self.quote_asset} @ limit {price}", + "close": "" + }, + "vol": str(quantity), + "vol_exec": "0", + "cost": str(price * quantity), + "fee": "0.00000", + "price": str(price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [ + "TCCCTY-WE2O6-P3NB37" + ] + } + return order_status + + def get_order_placed_mock(self, exchange_id: str, quantity: float, price: float, order_type: str) -> Dict: + order_placed = { + "error": [], + "result": { + "descr": { + "order": f"{order_type} {quantity} {self.base_asset}{self.quote_asset}" + f" @ limit {price} with 2:1 leverage", + }, + "txid": [ + exchange_id + ] + } + } + return order_placed + + @aioresponses() + def test_get_asset_pairs(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + resp = self.get_asset_pairs_mock() + mocked_api.get(url, body=json.dumps(resp)) + + ret = self.async_run_with_timeout(self.exchange.get_asset_pairs()) + + self.assertIn(self.trading_pair, ret) + self.assertEqual( + ret[self.trading_pair], resp["result"][f"X{self.base_asset}{self.quote_asset}"] # shallow comparison is ok + ) + + @aioresponses() + def test_update_balances(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + resp = self.get_asset_pairs_mock() + mocked_api.get(url, body=json.dumps(resp)) + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.BALANCE_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_balances_mock(base_asset_balance=10, quote_asset_balance=20) + mocked_api.post(regex_url, body=json.dumps(resp)) + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.async_run_with_timeout(self.exchange._update_balances()) + + self.assertEqual(self.exchange.available_balances[self.quote_asset], Decimal("18")) + + @aioresponses() + def test_update_order_status_order_closed(self, mocked_api): + order_id = "someId" + exchange_id = "someExchangeId" + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.exchange.start_tracking_order( + order_id=order_id, + exchange_order_id=exchange_id, + trading_pair=self.trading_pair, + trade_type=TradeType.BUY, + price=2, + amount=1, + order_type=OrderType.LIMIT, + userref=1, + ) + self.exchange.add_listener(MarketEvent.BuyOrderCompleted, self.event_listener) + + self.async_run_with_timeout(self.exchange._update_order_status()) + + self.assertEqual(len(self.event_listener.event_log), 1) + self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCompletedEvent)) + self.assertNotIn(order_id, self.exchange.in_flight_orders) + + @aioresponses() + def test_check_network_success(self, mock_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" + resp = {"status": 200, "result": []} + mock_api.get(url, body=json.dumps(resp)) + + ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + + self.assertEqual(ret, NetworkStatus.CONNECTED) + + @aioresponses() + def test_check_network_raises_cancelled_error(self, mock_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" + mock_api.get(url, exception=asyncio.CancelledError) + + with self.assertRaises(asyncio.CancelledError): + self.async_run_with_timeout(coroutine=self.exchange.check_network()) + + @aioresponses() + def test_check_network_not_connected_for_error_status(self, mock_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" + resp = {"status": 405, "result": []} + mock_api.get(url, status=405, body=json.dumps(resp)) + + ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + + self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) + + @aioresponses() + def test_get_open_orders_with_userref(self, mocked_api): + sent_messages = [] + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") + mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) + userref = 1 + + ret = self.async_run_with_timeout(self.exchange.get_open_orders_with_userref(userref)) + + self.assertEqual(len(sent_messages), 1) + + sent_message = sent_messages[0][1]["data"] + + self.assertEqual(sent_message["userref"], userref) + self.assertEqual(ret, resp["result"]) # shallow comparison ok + + @aioresponses() + def test_get_order(self, mocked_api): + sent_messages = [] + order_id = "someId" + exchange_id = "someExchangeId" + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") + mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) + + self.exchange.start_tracking_order( + order_id=order_id, + exchange_order_id=exchange_id, + trading_pair=self.trading_pair, + trade_type=TradeType.BUY, + price=2, + amount=1, + order_type=OrderType.LIMIT, + userref=1, + ) + ret = self.async_run_with_timeout(self.exchange.get_order(client_order_id=order_id)) + + self.assertEqual(len(sent_messages), 1) + + sent_message = sent_messages[0][1]["data"] + + self.assertEqual(sent_message["txid"], exchange_id) + self.assertEqual(ret, resp["result"]) # shallow comparison ok + + @aioresponses() + def test_execute_buy(self, mocked_api): + self.exchange.start(self.clock, self.start_time) + self.simulate_trading_rules_initialized(mocked_api) + + order_id = "someId" + exchange_id = "someExchangeId" + userref = 1 + quantity = 1 + price = 2 + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="buy") + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.exchange.add_listener(MarketEvent.BuyOrderCreated, self.event_listener) + self.async_run_with_timeout( + self.exchange.execute_buy( + order_id, + self.trading_pair, + amount=Decimal(quantity), + order_type=OrderType.LIMIT, + price=Decimal(price), + userref=userref, + ) + ) + + self.assertEqual(len(self.event_listener.event_log), 1) + self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCreatedEvent)) + self.assertIn(order_id, self.exchange.in_flight_orders) + + @aioresponses() + def test_execute_sell(self, mocked_api): + order_id = "someId" + exchange_id = "someExchangeId" + userref = 1 + quantity = 1 + price = 2 + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="sell") + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.exchange.start(self.clock, self.start_time) + self.simulate_trading_rules_initialized(mocked_api) + self.exchange.add_listener(MarketEvent.SellOrderCreated, self.event_listener) + self.async_run_with_timeout( + self.exchange.execute_sell( + order_id, + self.trading_pair, + amount=Decimal(quantity), + order_type=OrderType.LIMIT, + price=Decimal(price), + userref=userref, + ) + ) + + self.assertEqual(len(self.event_listener.event_log), 1) + self.assertTrue(isinstance(self.event_listener.event_log[0], SellOrderCreatedEvent)) + self.assertIn(order_id, self.exchange.in_flight_orders) + + @aioresponses() + def test_execute_cancel(self, mocked_api): + order_id = "someId" + exchange_id = "someExchangeId" + + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.CANCEL_ORDER_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = { + "error": [], + "result": { + "count": 1 + } + } + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.exchange.start_tracking_order( + order_id=order_id, + exchange_order_id=exchange_id, + trading_pair=self.trading_pair, + trade_type=TradeType.BUY, + price=2, + amount=1, + order_type=OrderType.LIMIT, + userref=1, + ) + self.exchange.in_flight_orders[order_id].update_exchange_order_id(exchange_id) + self.exchange.in_flight_orders[order_id].last_state = "pending" + self.exchange.add_listener(MarketEvent.OrderCancelled, self.event_listener) + ret = self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) + + self.assertEqual(len(self.event_listener.event_log), 1) + self.assertTrue(isinstance(self.event_listener.event_log[0], OrderCancelledEvent)) + self.assertNotIn(order_id, self.exchange.in_flight_orders) + self.assertEqual(ret["origClientOrderId"], order_id) + + def test_execute_cancel_ignores_local_orders(self): + order_id = "someId" + exchange_id = "someExchangeId" + + self.exchange.start_tracking_order( + order_id=order_id, + exchange_order_id=exchange_id, + trading_pair=self.trading_pair, + trade_type=TradeType.BUY, + price=2, + amount=1, + order_type=OrderType.LIMIT, + userref=1, + ) + + with self.assertRaises(KrakenInFlightOrderNotCreated): + self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py new file mode 100644 index 0000000000..6de2297f5f --- /dev/null +++ b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py @@ -0,0 +1,90 @@ +from decimal import Decimal +from unittest import TestCase + +from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrder +from hummingbot.core.data_type.common import OrderType, TradeType + + +class KrakenInFlightOrderTests(TestCase): + def test_order_is_local_after_creation(self): + order = KrakenInFlightOrder( + client_order_id="someId", + exchange_order_id=None, + trading_pair="BTC-USDT", + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + price=Decimal(45000), + amount=Decimal(1), + creation_timestamp=1640001112.0, + userref=1, + ) + + self.assertTrue(order.is_local) + + def test_serialize_order_to_json(self): + order = KrakenInFlightOrder( + client_order_id="OID1", + exchange_order_id="EOID1", + trading_pair="COINALPHA-HBOT", + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + price=Decimal(1000), + amount=Decimal(1), + creation_timestamp=1640001112.0, + userref=2, + initial_state="OPEN", + ) + + expected_json = { + "client_order_id": order.client_order_id, + "exchange_order_id": order.exchange_order_id, + "trading_pair": order.trading_pair, + "order_type": order.order_type.name, + "trade_type": order.trade_type.name, + "price": str(order.price), + "amount": str(order.amount), + "last_state": order.last_state, + "executed_amount_base": str(order.executed_amount_base), + "executed_amount_quote": str(order.executed_amount_quote), + "fee_asset": order.fee_asset, + "fee_paid": str(order.fee_paid), + "creation_timestamp": 1640001112.0, + "userref": order.userref, + } + + self.assertEqual(expected_json, order.to_json()) + + def test_deserialize_order_from_json(self): + json = { + "client_order_id": "OID1", + "exchange_order_id": "EOID1", + "trading_pair": "COINALPHA-HBOT", + "order_type": OrderType.LIMIT.name, + "trade_type": TradeType.BUY.name, + "price": "1000", + "amount": "1", + "last_state": "OPEN", + "executed_amount_base": "0.1", + "executed_amount_quote": "110", + "fee_asset": "BNB", + "fee_paid": "10", + "creation_timestamp": 1640001112.0, + "userref": 2, + } + + order: KrakenInFlightOrder = KrakenInFlightOrder.from_json(json) + + self.assertEqual(json["client_order_id"], order.client_order_id) + self.assertEqual(json["exchange_order_id"], order.exchange_order_id) + self.assertEqual(json["trading_pair"], order.trading_pair) + self.assertEqual(OrderType.LIMIT, order.order_type) + self.assertEqual(TradeType.BUY, order.trade_type) + self.assertEqual(Decimal(json["price"]), order.price) + self.assertEqual(Decimal(json["amount"]), order.amount) + self.assertEqual(Decimal(json["executed_amount_base"]), order.executed_amount_base) + self.assertEqual(Decimal(json["executed_amount_quote"]), order.executed_amount_quote) + self.assertEqual(json["fee_asset"], order.fee_asset) + self.assertEqual(Decimal(json["fee_paid"]), order.fee_paid) + self.assertEqual(json["last_state"], order.last_state) + self.assertEqual(json["creation_timestamp"], order.creation_timestamp) + self.assertEqual(json["userref"], order.userref) From 4193e81619c23e994fbfb65d9025d10e519b1f4f Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 15 Feb 2024 18:17:55 +0800 Subject: [PATCH 10/34] add orderbook_data_source unittest --- .../perp_cross_exchange_market_making.zip | Bin 0 -> 24394 bytes .../test_kraken_api_order_book_data_source.py | 520 ++++++++++++------ 2 files changed, 338 insertions(+), 182 deletions(-) create mode 100644 hummingbot/strategy/perp_cross_exchange_market_making.zip diff --git a/hummingbot/strategy/perp_cross_exchange_market_making.zip b/hummingbot/strategy/perp_cross_exchange_market_making.zip new file mode 100644 index 0000000000000000000000000000000000000000..fb9c3be87b6242d4a9d635c9a876880b0b4686b3 GIT binary patch literal 24394 zcmb@rW0Yv!vMgA(YnN@?wr$(CZQJH98@p`Vwr#ujclzCX-srwP-ucl>bN$MZGslX^ zToLk8z#zW>{yC}a{F#DU2y`H^?v4O3#g%Pd2$G`HT#TYY5`kT`~>;EIKEA0zAEHz=?C z5k$!N7FT$QTA<`*nQSY6PDt!hw=jHldCl>C4?P{nqj#8}8#W=ww#`yvKH6!5_xurB z4(w?dGhe1d%ZyTnf(Ot&ht%Ul?5}%6pq`M)Gr?i$nt6M>2phpay9_bou5?dwK61Jy z+#D?}uY$6p8VMrSg1p4sGSAM9mz2d3hptt6W%^_mqlJ(jLmIzi#dAv-)fd9UluO=% zS8KhzN@gm;jnJ)fCH)W`QpNS?DfS}qmE-R_aBH_*6G)|eZFzz6rGa;IAk&J_D$4hy ze%uSz@yJB9brn9lgTFUoj86>27}eD2i7UnDuEGVD0+3>M=-rG_O5&CWojV)vWGjotK& znZ)}Z3)f#7ekim(D{GqC)9!DLvUTy7yrdtXNrR&AJBbw{c7?aR!C#)ApHUG}0()Ur zsL}N6*%KK96efuEL!wK)Ky>-omLYYa*AlY_(xIg&`G*I^w}#U6=NtvGmrJ&!4K^6o z{A$$Z#n3GT7qc`xkJG%)78YX@`@AnZny6M*(-1}{p*?&{>{93MIk zp-~lMluYlU%w>-e%9R|3i)Z6Yq#ec)rbs8??^EU$h0Ug6P9i6uz6CyQ=BzRt>kqm( zo=f62*JR}YsT4jirJD5-%7s9z=0%~%Ol!0~@^i`>yMx2(>30|{NL#%Lvry(Flhhm+ zquoSPcUg&Db|*I1YWWv1dNk^F;I&+5(PC;9#pIoz(^T`>!{Dclotkd(8{zPxKZtAM=HYG%?GzNY+5F~ zX@i{HUM1>g{N}Vg)7L)15w9)FS3JHd>F#DE(Ct4o$sZ_&7Da5lTO6#4XiG3EtuFYgMuUKV==&sDuoCKnQ}P$^G)%b?hNbyD^tmTd$n|XUBQf`0Kl5aAg!^n(_TK? zg7dn8x(leAcws*NDPb*jKzX56v*k}6ZOcrWiS8*7UdD!Fh{hTQ{kwZKM8=$hHWqxb zBS{*RRRSZQCw`tv%nv!NRn~Z5Luf5a$lr^uqNGkHR%fDiPX5cr@D-SBN6Wa_mUVr| zRn7S|v)tDp%{%ymv=1a|hFk zM=PtF=?RX(`(|i^tp_i!+wbdH#@^G}6+64r$K!Lui$2Oqlw1hCIAclcRcy?iw8PuI&W zR3I_#<9TtuY4~@3iNqHxPsqaLcy|E&E=g!d z2?AzO6t46%^Hg*UhgyNAlV`OW5QH}eJUL6$!X50u{i|ZeKX8(4^HYj7vzJ6fH%R~xX z7~{yEQU$>BhzNTI2(jw!c!8!~1YkpzK3RKa>XG8U@F}3#6jv&IbuF~K6*sr=%7u^> zhJ`8RKhW7FA3Yq4KU)(#|{+kNNq_^cRQIZJo zDWt9f5JR|P-T>%5d~Vrku^}ORON`=n2t*Lyc&4(zwJG(WQjuoqZuPr2VUajczL=2v z`tC3|)~QF(@i1WJzmP~+?&CGKt%@&K82PEoDAZ1*FWJFv~%kI_7;p=lnd$e(yP-W!{TNjMCqaI+w8pz}n%w z0}B^pQ=V%Mkt>(H?oc5UEHlQqW0JR^mJM|+Kn9w9H{y9Nz5OW|J^rT9p$HFCYQo7a+krn5hC#MQ>t&<)8S5kobox4)WTcl_q z-O?xiV+5hdf;6^8#kwq1hN(xP#8!Lh(Og0H^{>VxrewbT(FezsAV}ws+LR#BYEn7k zHgW#7v{RoWD0}nt23ZyZ@Os7Wm%#dH03_Hp`Lm9moE_>o>Q=0W5_xUIEegktTAcgK zeE7AWt(S(I%_n=U7`y5gltg&5J9g++31P;qA})*$-U_b zxScIA*wSinQ7Vgaf7|@hY3vX-f3K-LI<;p$$~{v#2!|Va+R5<_&*Xb@p>l zB&D30T1n@CbNIWxl6{Q4(d@n9Xw$DhLyE=Pg+^DG+2i>8)l3ZHcXV1#MF;NSTy?r%Jwc`q~5rxVDs= zxmE#$!10}jQI2-nLtM;aBy*N=+g2-GeJROUS2$XiSI@33I%GzcGd?SsZH+r{4(ZSz zgDakbqFhK>WjWst2d~BC$B|mW1y){RBJ`WM^-SRIUkqsy@lU!W<)Em{dye30*AbWM*^3E%jgHkAa3 ze7hnRqruuq@!QJfUzzkpU;ehFU4NmZfRnyWx@tS*SVc3uhvz^+nO-t+I>=bwnz)&mIL$LcV8Aboy=s%nN zcNtq#1hy3r9U?M*|}(6UV=$asQVVA{~Zk ztQGk`)@DnU%Wi`nVf%?HT-wzN0u;)beiu%F(^!0iKnnqZ*`+~4s+fPC|LevpnIxf! zT;xnyIfy3e6V?!!TNYp*#H6sfKw_oEaCILxWoD>6B#j zT4%VPw}dg^Sstd@6Wzn)*x)cJ2Gj9Dbz@-(~5%-uY0-Ei?~D=$Y$-a-Giz zD-Ba~4GPn8YF=bm`xGYUCsaAjK}sE{3hP|O0l#YoI(6PKiB2RN?Zf>2Ut54kE-r?W=veH{y%@8VX40Z=_MFOc z#06vOLN0$gW)6Yh?kS4>Z0im=@fub%qSK!P^9(nG1Ob=h-Em3YPSHV1KOsJaGpK9a@k{hQ+i7BLW!ZW1-2y@uZKedm1}8z$ z52p5O)^J2Q33lQ+#e^#hvfRXzSAhBv%BkOKhzF-wRvS}om_wh%DA=sTHz;DJT$VkMr;BIwe53#BF9WF3CYFEnP#zSL@0Eo}7OQ&zrX z7)_SPiEgV0a&?D1SQCP~S6Mj|ism;CIlQDCHHS<_!KsuRMwyUm25ItvF1|%NJG%#{ye#s= zJG`hQQLZOngHi8gf;x=o!E(*R(~STOxMK&Bv-FgPL=O;J+|&gBWV67I&Q{X{QUV_^ zdcj^MqfV43og%IDnoaEJ`HJpva7O)9eFFZ2qtV#L%~l|Qf1%~yIFifyH=XGH#S!Iy zgd-1O!yGszKx~d}KoM{hQ zeL9j?2~i0^UNQi~5VqV2>JF-?Kw|j@9UH6{APuao&%ua>Xn854vy4=8;iN|^%P1M|@X{uiW5JLIg7p*8IRT1q8q2j)0 z{Br%zh#7bq-^R8pRH3u51fH~H&>4C$NE>CHd!6M|9*8*tL*73zJ z7ZXb6i}~9Kbo5i=>}cOL-el7cD4C@T4B)12xhkNIiYriON7eYL=_>6Ah9_A_qm{Z~m)dg1 zRtJTrM@96adAeSP6p?iulmHP5K>ad%l=ISV3Nx~eG`ksrGIIMN!5#g)F`tcNm92~- zjb$tE^&MWz2#!vf9Nr|=`K?l12;$kvPS`4>-O|heWTvX_GC&0gIvpWTO?J5*U+pV( zRj=G*hhbYDwNhX=Dph1eV_72Y$i>-QcHW8|k$i-@?|?VXf#HcZKGpbqf$Vc_3f~6H4EVJt(}<)?2>O`%*m z>Iy$-z)NCRtN|X3*IT(;XIFm%qYO!%axL20v<}C*Sd3o_Y??wiZcC;8h0JPN_3d2@~2tMD7_*ZvE13HqLGPFU84^er7ZBWIv{ z6@uqr#gM7X_=YF#Y&sWo|6m;<^fu2K7y#fOME@J>|9j7b_{#+UfAmauHU1i9jP55DxwmYK%zNQCV_klgdBRL5BX1&nb!PGEhzC9Ef3?;qa!fnet$)J#A{ed1#_Q z#^IA!XYsfFBzbSbyv6y^VxZ*IIq=SyHmo|mOYErEXHUR~ecE|)>7X8zP@6$LR%wmAYqY2^8i&CBqb!I!NrIr5%dX(vyab5Cf@MWa?_rY><)4NEWkR_b*b%A~|y zs2^pr!1@)5dedj|{Ot=xtyQvvY&|v944HM5et9f!UaXqQE^rTp@A$qgi>7$PY8nn> z^HEi*K77CE{C01ET&RXSbYpkM_Gfm*S~rQU8j|IpYZazX+Rf}Jx384GbLC`GWYE7T zKD_)FfBWSfCW-Sgd&{yTf0Il_Sx{(s2;Au2t5sIa+H)^9HJ|&Z(qep$l@F zKn0W@aj9ZYPMFLEUo^bXKfKmvzI+D-^YKyJ)lHclWhYq=kcP>dhwtc`EOCkIcqSY* znhZt9DMO#e(9J}A?A;C!)_$(gIhNi+*5X+EA>fMlWnMIRrX9mpGRn$3($}7SNhGmM zAUxeyOpxX+6;*Xp3{!m5Tr968|E?-~d!0XP*!y_^Sj0Yp?dS|hX%Qb$(O4-bJmM&f z__@KM>uMJsR~umNIqX3_+c!-_k>UMw5?p&U#N>~U1|~B|RrL*H)iaJ7+}om|^Uxg5 z&TYC7xujM*4hc;l4;%Orq+${D6InykpuKmG;>{l7SGLvisf9%k0>BIz1k7&%d?^&}f2gj#FWl+p733WV5N!|n`)v>#!rAmwww79Wdg8>Bx%Ba?FNpK# zlG|#YkSg&D`6X$^q_5*$pZj1T z89OD-4z*YRy$%re*&aym$J;r%o5R=FJv+Da^F~P53~dJwuiNusVq%4Fp6&acqMO(K zTaPa`Ys z!>$X84~O>$k?J$-+=ohKeC%wak$Cv^`08wet#+Y^jjse;ObnZHj)NPDj0hZYfFBWy ztN@ABQ*cK{I;8tU96F`pwgn2N00|lv7lHG(>-2oAud+of>}+DCG;E{?8@qfB0_FYb z=;FlA7gX~VS%CBH%)du`>iFB!{WaC-?T9Yy>>)1>9^j5XrlV-<(BVE=pO4G^>++VI z=W}BtZN$$l3h}UKw|GXwNk#S+G_4JZ@Z#HDH7X+H#8Fk(K0^wr%QH`Ise`!If#g*3 z7JWh(!n;Un8CwC1LQm5vwFFq@w_!F5T072CW#_2DmfBk9^@=qsj%M|!nA28QGkVnr zx_8O8ki!l2ApOihv(;w)d?cXJen0Te@U~eak0B2>;*Xmfo3?^JIZZ;Z`$Q)#on38} z{ZeHVvljJyq4m85?{xmRu47+_uI`Tr>p>^8@vZ(bW{W6wNu3A z35YGy`U%q5Xwv<@U+W{AXTJaCp0$P&;ajMCTUA{i_F{jviCk~A9KWJ05l^2WEdYE9 znn#kf6KfW+cCc(s?W?qZwhFMe0=tD|jes#)T{Vh?(-7o3M(&0r#DzdK&{!-?f&Wb8 z_g*sN$n=CylWC2e_ir9u>*;~PAH(aFAf85lP-Fzd2#AmnKb7aC6$Kh?ez~^!IlibT zI~A%EU~tbyC{HJ>(=}^SgNKkz8QV^m!jem#O3QqL(roDXDEG4^nJF}`$_Gx+t=Quk z5iAKNC0deFw23Tbh|4R`wvXQBZZUYb2@gR*ftb5VFTd4yowd)}r(Xyv8Tekw?BOfx4BON4<4jTX# zM!fXRXQ8Cs1JCuYkU#{aNd|okKqYumPG#v~z|T#yD(|p{S#4CJZBD zmtl2H8X^{TRVE${_DFi5m62>0-j~TouRF4(Der^Cr>F)93&F6)GS8d<{hHN%{1NmD z?+|Z@P$s|V22}g?G4a;~&)nH0M@$CGD*y%4Vksu0XyM%W9IZkQqT+B+OKl74QqR=X5KhKsyVjX@!|hW|Ecgme%<|D8sZGN(mzs zT)$aYuJUz8u`r^7z@h*?IMk#WCKz0Yfkvvn5;2ZLMD?~R;aq1K~jWEan@HCrDo-BV>=O($pZ9I-wB-sKJ@dX0qKXS{L$`$O>BY7EoZMg*TX;@fxG3XkHP0Lo|Kb_Kqmh1BwRnUTamXEw-xP zv`AXcuT6hF`4iOKamoB-^C#1DN!KZ3X=Cr53UJ7a-;zwvQY2@ut!|N?>uOIGPc<+S ziu#XW4Mm1pG&A<%-p_vLP@|tgVO%rQ<5f|sb2MVDiCFl2%_A;-C^+peGZ&?^B6mNH z;a?qnYu6lPmkeb}n64kJ-OahoI$rnjQL9Oee@YuMA(^*nc8z(92zAxDg3D(==Uu}s+Ot2TRHo&>!~kB4$Tm5I+gI(oY?e`bas>|1q}Rqjz-9-Q zKR3Q{t6Vd=ld4K3;oeg%w^qr}8UfSI(O%Rf-a>|i?c4oIyKvNq$#}C&$LIT<_*$wW z--SWQR%&>grgDx~q4P!oIcp&A3Lm?QUis(P$#7*W7F$3tXtU9rS!z> znwUKpq_gTKMS%HbvN_?A&-3-2*&foC?(Ou0F4mH_jI{cdmGjIBA20v3)7bt~b~;iMNYX%|lTn&lk5mtc00j_;Km+6U6W8m9_!k|MVs z@dV+W)IA9w!6sA%#YUz-n>C|7S|6sw=$HD8C)uj2UHYTGQt%I+lY5tw3VJ8)nYlRZ zL2le;XdHl`*rm|G>t_zA#SwKR2Bw*tDx!TqM-TGxoPm6T#3eKF6dq^HY1A=#r9#Ll zRH7Z3F+~Z2Lm%hMV8)Q~WA@9r$k6W#-bXvUZwyJ%db)4BI&`%GMQt!=0A%g-3vsGn zw^>P9Zz3E)J-rk--+t=MOEUMU)6;uoZ!QCRV*AeU-`{Oz>9+yyTG-8u)Ji#6k$QjN zRx6a@SL6{?vSOA@8w+LReBheas;rUGop|5sG-3ZhCQ;jT3Y9J;_5tnEi#k(hdFNe{ zTB|nt=WQy=__L#fwk&r?U>w$+0?oZ7@cz1TjO+u$kmgLmC60^XU9H4YgLjVrQIoq6 zeyaq;tbhMSO>mdC%%Rr94-A{eB#J=Ge*o#11itdE20x<;X~fX6fX9004M%9+FT7hD zI3olyh35}pJ2QWT*sYvELllX(mG9#x6q@4Xb1l!7Qjg3l4M&96!~%x0!bZ9 zMp!XU+QUT13_;c9e`-`9NLy<`C&62DNnaHvH-FN4A^^v$jdjlaM=p_;AJ=L;apvQR z+#?kQKWlDqAtJ%9q+Ya`0w<(Y^i)jKdM~&l0VzKWRgpmHa_|x40Y};oq*j!+P)C>G zcWj!SDR$j@2C3x2wg5HJUlV=N$0s3naJl_+ut-7=_ka9uJ;GUR8;H zJ9jn<^-p1XVLbu-D?!@j?|{D_JMa#x_}lh^@T%%w zzHccdd0^zz3_wlYG0NAkLVP2Jni3$Vy>mRpiaF$dmiURnD)me>AFc&DR8dHH>kChDq zK(>nY@F}#Zn7ESN-riTM^a3@Qx&eF@HqDXnz}zv@+Qv0$=)M+{>%Ib9{j8QO!KIt! zIUZ?Q^z%jS)XLQ&sMhTGcX2rMHsn+51A#jv_M>Q&uav+cfM2h(pH>9&nnkMpiu*x5 zEe@&-(&VSY87qBcnCcLII4xBJMh-T*K}6Xrkc|e#NLS?~V=W0F@B{`$0Z_ zpRW%07kzgZX|;ab0eIn(ScztYK+ZAWrIS(J8vL)9q9gGWXIiWZ(HwIXt{Jk*jq(Uq*tBx+qn{p;%zYD9{MN;4?psetuzD)zCIJY3(|0u)K*&*sZ^##ss1z#$rA7uHP{mH6pOS%CDx?_Q&IY+pQD`i{ zRD!7Q3qv}wa&&jKx&f>8LkXhKp~Dr({*tLD-ywb68C_7T0{Q-!Hv=Pz!SDeIZWVwBQgqNHY@ z9*4m2uqQ#c8auRn@_qj)7Dq^LGNQVVFE#fff{1>J3x{jAT9EL z6}0XdxGoEWqpx2xQ0!KM&JKcyqPkJ~HINaRssK^or796Tb@LR@9q*fRG@28+vQ6+?t#ZOiN@OrnjY+H%>> zKoAl}zu6;4EY&8)o12xnzhtDr2CZWga)5Bi!y?vWi{IqGoV6w#HAwCh7VN7FCQcH} zDP`>M%+OX`w;6hdE8$`i**sm#TB(K4%faQn>6cI|kWEOA7kEiba-6Q36ZomB@0*bD z&>@bu0b|A04U!U^7hJ#lMR5kXo^8f91{wx|q@;3>s22o45a6r zpiHh0@Khk3g33@Qj4>1+LC{WX7^>EeCPZs?EB6=C@{4tS`c|=yVE$n%eO6&Oz*y8$ z1wh&>k8k|c@C3%~56nXz07KMbWvVp|G7luq5;t8ivNKJ`T2%oJXK+#fDDg%ZDs~aT z9oRyO1=UEj?1mMlj~{5 zvutgzc0E(hQJR_}k!nGQzd}0IoKNPq7@iT)$72~2S#Sv%NhN4yHGYe@hCVA!9hokr5=e1;j5=*Bpni-Pk!3GyO$Qj%Mc=Y9-MCLuQ49BS zN*lr{ePnRo((?t|sDHo3>7zyq?v-yZg-o@SYx1j_)}_I-4ZtM@{xi_wG7-&0A+SeB zHAcV`J;)yhJ?j`xdI&p?=^A8_YSvg613ANoG;a)$**`Ntf!OH{Yf$) zIRu6L48sbAZ$}qq>FTv-M~7m**96w5av=bjs?Qi~*aA`9L(raHxF#mBF2ygI`v6tg zvv2mYp%Xs~d6U54P2AW8Gg8jJzpSWa1+ zn&RT1G+;t#5(+T4fL+EYC0W`um^!~C37gtvIN_B7(xeN)%dt%%-iCvklGrfRWk>cG zebanf_!BY*zgI!EN~>cy^RY%kfVi6j@}vwmh!+q29}AAGl$FlmT!I;19LS>0CzPP8 zfK4X*O7T_jZ4@FQi#~F`Gf?EuMMjlxi{dFS@2td>QTlKfa*y9m+b5Jz+V!-iY-390 z7ePY85oZoNH;Y{f1_-@&tuTqBvESX-AYbV421Ui`F^hK+3jRK^Sq+(t^S5Ed+5(Qf>=kWm% zi$wKUBp-E>h7e~U4AN@2C?Hk(_L6Y`gC0HTv?CXkrHK!N$5(Zl?mn=pbC&L2d{9vW z@=Xk|CA$J+ClwNq9yq?;wO@E&b7KbxNYr^D84hAF`X!W$Wm;{BHm$J=Xlj;+zIj=m zd!a>ZN<$?UG{V!uX}XkFRSkFHV{SION=NEq+@?rPP)V@htSOv93$u;ngdWE%-`?S<%Ukv{_NSe z9EkzYtJCw|nTA&7wo|&g$MT^D*x<%@0z0lWKAj_){L<-gTzZj`1YF-p%96U_x-~o2w!LMM-K8_ zp0|)!{!igbQ5AbYteGifa4f}}f*BZXMDFznLCN!2j{5P!@}#p50sU1>&-n+k_H?d9 zRgX=+2U=hoOjuORk{L+0R`iD&Z6^2C=Hi0+1m2QkIHB*fW3=PH@2%`XhSo9P$7wi> zuOOe)US;Cj##HqZbceX73X+T*u(I9UF%|yEa1gKJ0mG4}rZO}76T5ee7hrEh{N9Hq zkKeXbs-M4)<)1V{+~%b2VRmwBd|~Nhq;}Ze+Qb#+8Wc-SoQxY5Dwx!|*vUOAU>e%C zup0^RWJNG?6c=H$8s|nl=30QT(``d%?Jebf`g7m`1a37E}A8eM_Br^kaufX)jW*nMThyf;`lS z%QIO5S!@EbYzIc-0m5Q6MbGfmVA3b4YBh=K7oF=E5m(uVQC;htnQ@&F3eqTr|L#+5 zg_l|3<%XfhgnJ>K42bvROC`cCf0*W1IBkx$&!KcSGIPtp1u0APM9|>r>>Iuk$S>Op zBHa#hVL(V9yv}jc{LYsInD8g%du)v2tZM{V1w^}e8?n!_4dx6$pBc}@_~10Msg&Ci z%3jvSPdCecf*h>3gz-f8MiMOnz*J>jw*Jk)kHtcw70&!E)1(InX$Z)zk`SW3HI-9w z^KxbDd_R=)41+)n>*EaICrPVN13bn-#fpI$pp4`6AZPiP+eD{-Yd&Y!Q2thH$x(!G zPxJVCK0PcgyWs{*KSas-5l2MKhMWz6tw|32_%kSq6+9Pgu*>01hELOX+XA{Z-!pCW zh)r{~jj?d(%$D1&BxDc#p{hE^Ey=jocP_b4eQ(lGW zQv!@2QE=#2XSajK=jr-*d`J&9s3&$gkWQ0%h_Pz!$(yIBbdd1Rd#HzV1sd>GrYKgR46|eP~x*a*0dsq4-A@ySrIBtsLxfi;~NvDFu z)4%2IdM>7c?skcwGeT#K$Avm5Dza;4!i1*-5__Vk$z`bgEwG-j04!`j84N$2EFt}J zjz
    DSMRZm`YsoW7y&SHh??t{nfM7KN4r+wK$v|>Auz5@YzgQ|TZ)2}K66FqLmj|Yyez~9+$2n^3d5#{@9W!5 z-vEOXWi}Osu zh%k^WIf;CR$6>S#w(@Cmm@NT2cS0^Co$4F=8*v!X7%tKdbtymjc3@|3h2SuP?)A*-KP+{D??+elj`XJ?p=ZmL4uR)%PMy6xABzm%GC)h1ELZCqT6PFKPLe_L6kwsmWc=j0^H1oV34 z1hjCF?9R^9m{i`*09M&*ZuFd2I3pM5*O+J9T2(W3kdSSPJ`Rg{r@^)53qd-`0&*4W z$&UHep$1TCk1UF}1IlPq?AYa>J&F^c61A9G9P$O=?rxoC!g1eJyBu{`O+nTCj zy;VBV45`0*4YgFr7fnM1q=mj>HK=oNLX8v9j_}>Sg~!fTy=DxXv7SZ8K8MW?%V34E z_=G*uaxzy=n_*@xl}KgxgCZ<#14Km5SjPXgw!@{5!dR2mvP>G)S1@Wk3pYHp*orxrg9)@At1iq3lS`ZERtOj%J6_E+;KX+f@4?Od zbO+&)buO^i0zQaj?*zYK+N4UR+*zuO$@O4_u|Kg7vWlS`q3l&0)5u;hg2**q7hj$L zD%IMU#p}Req8u))wIbNwh>3E53#&^S-A7AQ136Ob?vsK2>F3VJ-OaZu?#Uy_e;VYV zE^Tc$WS`1Jlq-CfBp@+(cp5ldsrm!Pr!&2H|2m_OW0rj14J3lA)e1r7O^nxUz|Ip% zrWX^B7o!o}Gp%K=z?(_hA-lqvza9h9&4Pcv6=YAQScwYsCx?>EQ5QO{F8U{BD}Q0k z^PAMJ48sI7B%_cTm><_M2jfG4&R+3>-eX!n^s;=8tVeu3pi~$ogj-pkPn~)!h1Y9q$X~3CB&PkDVSTM|>?U4d6 zYbq3Ka`|ae3KW_oF)*#@fXh?d5E`M&Ck9EbA_y`$WF(m8JakcJl`f8lI!o9yN$|s{ zX-!jwD)Pn(tNa)Flei@A+e%A$neYBo7yuV>D|ngaIx9p?p5lexxDYpbreUWx0jczhHWufw=0Qn@tkGPUXgbe(uaDuw9w1p55`q?|@JJfF zJ7ck7T-^~5A!Spk#pc^8rWDlEt>NSy(`w9Vvvxq2V z3*=YwT;++5#gRAm;+h#N%qCMFPvZEtb$wu*oD zJq+OaUfbjS9cZQFfysi8V&ow5$EhUq(aoJgd11Lg0OY)`qapZd*K_jq_}bd;Se+?^ z2Df}-Zda#*jMO3_5e;_Bs0rkSgnRl)U;>5cqHqWtkuWgZ z^cr3?c#$RA30NTv*DmBOcQ$T(=%t^42N{SyB}pE^m%FIVi^&@!ZuT}k+({deU{ay_ z?>daE`JWuO*@$g2A|PM&IGvM0-PlhGcx@pzuns+5I#>IF3`jOL6Kp5SsZ1Gj4@Y6( z3ZmGrJ{HbD^{CIJOt6RMKr~oHJy;47W3&`m9MISZGO)Vkz;(|j!`2z*J!-3Smcbpg zrvI^#r$d~(SDZ8ZOmI6(XDsgDyGpwN)oSVvD*A_z8kRPoGV`tF%mWwQaf9x`p9cBW z`R}a8B``c~5J2`-=;JV;N$6083t!#@?pIsnt%2rOuffNX{1xV_0sJNMf;z&t(;APm zpYB4*@z{bChR%CVNZCkS4pKv)G$beKQO-Xq z+dfdjDsd$O1;!nbYfWzGAgvT%uZe_^8GY{A%dulsLo$r@pRtXRS7m#%c`*^~oOtuz z3{6{)R(x8z-5y$*cdYI8;)))v_s(l;n06eu)Y5MQhYcHla%lIQZGWMnAzh8t@r!FZ z%o%$ajC;AOHKQPwH@Uf3%tl645UEq5(Wo_s1dUE*;>s0zKuQA^aJi=CX8+K0C!x6H zI5Ko4ls`7ZMx}tNiV17*S0gzCdXxff&pj-_r(S2NJN7Wikpkh~)NTS6xoor$oK9Kk0E3ZVpETprIGUiOy~|$8pe+tz%6x@8GK=SxP~^ zLZkk_O1aCRINE1X;JC};4#6R~YX}-NNO1Sy?g4_k2Ut8n@Wmkz+#$f?y13io5Fkjx zf%C6a<$O5zeeb=!U!JX+s_yF9soI(8=eJe25jae~sk+gJ@jR1sZYMu-mh9u2Pri14 zWnvkQq++3KW9)L&;?3hBG!P1t^0SSyfnVMwtL9KqbA=E_EQ)oX{LJjJV6!L}sn!6n zTg@r;d%2j?=H}_17Sy2AFv+}NE&LNG9MT%j^^2L*>k_W&;nN(QEzewD8!u~NL6$x= zm}Oh2aIVSoQ+nnFb(rmz7BT3xMLn{S=S9cA`@&NmkCruNnT(Grn%tw*OWM*^G6cwHHWFXEP;T~>M`)Fs#&iQX6E5!wT1P;rUlP;4w21*w1y$2xdZ+{X zi+xN5t;;-xyp>kPkg8}!wRGYoL?czt$O!$t$3bZ3_BtV6MK_Zfi6_d{$i31{ogBlU z|DG4&1j_yZ5@|UQS^}zIESfOMN1(1uu2b>?qA9Thn={0VV04rS-b5Mi;HG>~_VlkG z(r`XOhlWZBM?1meTTRvEz`PxB{m`S|Q1=6LPc0N8Q|XJ9gYxjVI$L8Qe#! zbe_0{4|b^IJs~W*)z}2%IIt$O1Rn)2XKi!XX5{=wZI&X<&TdPs6?0C>pf>ytCXYak zESlM=VeITXMZ!s(F5w5s*u(luvkG{uwB{Bu?HL-G537a zeYp{yMb83Cw#_fKv+d)x;U62Y__k5i3=C_1l@vL76V%WbJ{8{GC;Ll5Jg+`TY{~?* z#Slt%x^Q0fQ6?E7yd$h>GH60wyC&F1`^>UAz{)uxWbTZYp; z1U^209p6U{Sg&Ip(uA3Xj^H=6)Ln}--h__>7(go)$(Q-;ggAb^k(*MnYgv3m$`RSY ztC?R?AkBQ|jx4PeACv*?}oak-Zpo$+IMW0Ig`FtpM&L#=`-PyFx z7u`3wH6g`K7JKoQs>Mmx=Yfi6#0ViMTv-I;Q!`%8vlREe0fD<=aZ|Vyoy3p; zBuRe?nXl!3=mV_(iOB_=Mv`B1}M&rW8t)<22>V zhTxKe1Sa{)#ye8j+sz4sg^y00ahTm2{=DB=7UW=u=sSdaIbri})3i*U{_cA5)D3EB zOyk9;m%j8!bOu zTFgyynEtT(i6T>J{GNKLK5Q*NL2e)IDdSy}I)gBx~h+Hik2s;qS3 zNW2nkr+j*HQxE|r((;n<^Fg!ye06+_1d@CFl=`lC$vk0|24HpDG}^t{L^cku&~MFxwp-I7OUB?>SA=SgYunovq;uL zToFVsyATwso>T(@`YMY;pvi&9A+?3rwtu;5_-1LxFo*oyFxp+K<5@DK;w8UsPj;R; zL8Rncv5$;C_oTEsMvC3ASwvGXkHSh9 zaGz4=-Z_hZbD@9Lr+YlW9nvvmtDZqUz|)F~y@0Im9VvNFS`@isCPf10v3(p3i0{@T zAyMbg;T|?;spiyKbEl9#aEOE4E%aqq=CX$$n@guBRT*Z4xuj0e)nKEKF;bs|oYH~e zrdsuZ@48L)M-bR-jO*!t&YYpHLP(GHYP@Q9?!DH^E!n>EqrV+cs z`+?oxJo}J#X3~DX^w`rYC4hb5Q$D9-X&tZJBmim(e64^ifT>QpcSoM?tJ;{#c|OeQ z(#q5;G6fgxVywSYNr{O$yL88if;HQ2rn?$MC}A(k@LH?4!v|a@WJ=!ut6TNAsY;d0 z!$&Y`1HGD$bnr$oxh^=9W}E0_MH?zCVgCCY;4{0G>JO=dL@Ar#Nj*y)l?5i^_VVdy zc~Ya7DCxM;??6f0#>2aJ)fG6nC*52WJT1tj-$ce94U#_(H}t&RLucn5Jxus+cA&pJ z_l@Sz3^~@6-rZ<2RH?{8*J0jyBl?K7OmP z@9y=lRzVJD;E4@V8FhoL}J@#FLB-7eb6L(3fGV#(FH&;;v%t;oMeqL)N zWG3B^KK#_kN>mQjbS@Vl2A-MG$yvZTwywSk1!&}2#2|v1-UWP`-<V}*E#||!r;G{%IQfU+yG$5n_fp5gU`}uuk(n9a?U>}b7h5utQcjQc7U7#hpPefyt5_bV$w{!cQ^fg-2(wPMl(wQ5vmf|LG{ zsA1H1O};`&mq1-CPefan`-?nW_};0t-fhcn-Jf47EM603IOE){LEb7t=QBf;;^{mEk#c2^ihyolwxX(sy@uYmO>K9sxCM!NHcNNM?Ph;#|pkPeT z&k-JrkIUexf{i_H&4L$THXn&?krg_qpWH!-B5(|?sw1)3)2zUmQUU3tHxKW;%%Y>t z4pDd5q#x5|6QyVZeAFCcpxsBKqHrQakSJL})nBMaWKx@%i0LHZAb;nulyDC&vVFa?r?< zkqQX@aN(tE&%P=jN*AF1!ln>ssF{LcWtDb3WN=X}m>0JZs&4~Iz&38b-Pt*$>im&a zm9X!52rwO7iv1RV+R#>Hbjnw2KQq0`m6djC`I8!F*{VZK*sq=Yl41!4VSm-! zsMXx~==g~DXnDn0W-(f!Wu8E9p?1w~OT}uDa@vO=_@?poV}Jy}D)4!?&ETLJmh+ZN zr7ikc@{vHnU=Av_kiHfoUMYyxv({}%)i>dWpy%Po752Gkf!QT%#AUm*p~=?$bslkd z*3Mcz$=Vzf6MyT73C9(TqIg;Oam{;O8h3@Lx;kBA+)OtQsktc5=_@i7PrW77-Fs|% zvkk|-Wqpk82<)bv`Z+dfwvd2jPqHLGv%ZcZ#gLu)>sjdw)Z7QCxr+a0nX+&t!nK{L z;6ukTKl3DsWq|knHj1C86&Tg>9U(gM98JXzsoOVnAQ>g8E7Vz*Qug|HP9uV#M9*1K zk>?96uXD6;>_t=|94{=x7e{mXnOK6>UvVoBDr%7yyhQq~U*}pIhuTft%=g1-8N6f2 z700dfjh{%RwYMa3N~&xk@2+v{Xi$O)WL7#&MAg(Z2t#UweAK}vb%*g(ORF!6FcL5I zZ=wAm%UK#Gdz<60jgROmea9yU$Gh-dopdD@^`upgGs^+*Z4fTf z040g@kI67tmHWKpp|jw)ddpF1&(8tumuPJ96PSWK9VW4d>N~#tBp5|2fauKY7HiUb z^16}Bj|F_^q!kIU@Gt3Eaj&OE>iFU-%fGbf-2%!oB}I8jo$W7;5s`{F)wTqGDN5ya z#x|Oe7}%^OBmN>kg-(YHtyo@-;v!WNMVVo2c~R=tISSK58C{fD_P(;G&8Evt1IofR zCCuG9*@t5 z<{E_Mk(6#TDo0IVtenuNBts~wdlY#&||e zxTafE-&7j8w)8FpiZ1G)Z7d7#^!58W^vB?N9(it`e`-~KvR+iYr&Xm=Q@Ek^1U1_U zKgowu-vNc0o0m&$>7`hVeGLew6ZG?>d29$_Dn=dHo<%zlJ#X5N8w_y*$^iGN$9*$o z2ZE5&QP?>A`LWct;e*~8?*8nX*GODSqkRr!uYo;6gYBUgU|yF%mjYjDy@?qd1>xGe zRTTwTYI|Ty4#JI_YC5Kf8`BuSx1Jrp^V5z(R9-#YNDlSa?hr0RLNgeI82^Pn;-(PPgL3=~!xt>RG9kA7VUD}IA_-!uZ9)Onf6-38%``1Fd zkG|@bTO_xz-5COC;(i4%uG8=op)C;1NYXg;0)O0DC`|gaWzaGcM{!XnN~~h7v1)f( z+cGEDN8&W|=?iY^#&oeaMu*20Q|(SwMI@(8FBfB-G|#nyUzeKZA)A)lYAL-N?CG=0 zH<|Ang*IGnxSJ>(2DT1EHO>~}TsMfXxLj*@-mBJ~ei3L#*^}U5H&E)-8M)wE4IjFl zqWeCY?ne%5Tr=s7BV|$8u)GvL#J5swW5j0gG%3b?jzS;iQXo1>cz{G$mQH;ENqPlL z3&p~li30(ns|zXV&Sdz-WW_# zcXfmBd;H8al4q=jrwivB-3J>jEuUu_*oAZbv<^RB92CNfCHqo3&o3(Nod(^0k8URrIdab4(a`JC!(4$%SnFGuF z8F&g^4Qu5K3zI87QnMDh+j=i$2H&zwl@4;Pp(}Tib;~pMKDePrMWfznzMP=Kfr&X?$E1cMzjBAgm@={{e z1D0qr)-!!mNlKn`xQn4ZiyjLv>Nnne-K;qYNQ6Yl#M|Y`*&%5;jU;_C`@q2hTh?WN z1S9hkb!v@x^`zEB3cvU^G2ABOG40FQtR-!_w#3#~%Eev#(uPtoEn@f5-|NM}$2F%CQ+z^ZtAl3=0>3ThH3&Dif4qh6E z-j?Da`jBe5J%-sEel@GHp4OxdE|0aFqH{kU{KS?aQ257u40_N@;$E1TZxqPnTC!wq zjB2rcpS$=L2Nr`4d2Wg9ORVPTnOm98>^7NNPX5ppyJf znNu`YZ(lCvhEv{$5{tUPEseb{R>dzQ=~bjUvnle^NTR4SV?;FQy@jEtTok#oka)!7 zCIUuDVy&67n0_6^;hB?Bag*5<@EUl0JI%~TTbPhu;czX?x;j?WHzF2BAG!%c#}t&0 zZdbIowzXwwUfdEV46tC%as&lA@Xr|HFLnI*?TW~i zyIQFIbjztD7YyPsG}AiREHjCoy>QvhjUV*yLK>0y?aBKSFY8=v_CvBv#H%FJt`{sB zCXF$hPRG^22N-UpqGI&7#0Q++OGHXtdkl_4Y-I-`c}OAKYuCvouq1;CC*Jho@ZDO) z%ta?8^~bRW>iehW`<$QmArg7Igfz{FBhCA7_bPhUsmKN*Pw2b&B0$otq{JVCxtogZ z0Co1sHw(>-bRc-KSs&1{*HUyGfz>TLQcR$B93i@cd6SV{?K$+q5VZwtB6Bn5hezEg zJNEmoMCu~_eV;n>xhFf$b@8FA3`>RTM7*iQWak(kzm_DAYKY}EuGe~y1=b5=&c$5! z(fX1-+JoWDVO&sj9kkdeL$9R5>w^`C4@{DZhG#2G-g-t5ly$ypB?~c!q+yx)S9TN= zC6Y$l$@XkEQ|2!&V-pJx>UmO4&OX)I6SPefTKyD__1c1W*U ze8kex2vWHh`f7>C_v`gX_-DLAN2+?NT^t7HnN3htgoVS0|KHi|KLpgV#PjMO`GkLg zaQ`RYABOvHgX@{${u6~kCQr+M&i9W;{6C`HfBN!V=d%d*H None: cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + cls.ex_trading_pair = cls.base_asset + cls.quote_asset + cls.ws_ex_trading_pairs = cls.base_asset + "/" + cls.quote_asset cls.api_tier = KrakenAPITier.STARTER def setUp(self) -> None: super().setUp() + self.log_records = [] + self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() + self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) - self.data_source = KrakenAPIOrderBookDataSource(self.throttler, trading_pairs=[self.trading_pair]) + client_config_map = ClientConfigAdapter(ClientConfigMap()) + self.connector = KrakenExchange( + client_config_map=client_config_map, + kraken_api_key="", + kraken_secret_key="", + trading_pairs=[], + trading_required=False) + self.data_source = KrakenAPIOrderBookDataSource( + connector=self.connector, + api_factory=self.connector._web_assistants_factory, + trading_pairs=[self.trading_pair]) + + self.data_source.logger().setLevel(1) + self.data_source.logger().addHandler(self) + + self.resume_test_event = asyncio.Event() + + self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair})) + + def tearDown(self) -> None: + self.listening_task and self.listening_task.cancel() + self.data_source.FULL_ORDER_BOOK_RESET_DELTA_SECONDS = self._original_full_order_book_reset_time + super().tearDown() + + def handle(self, record): + self.log_records.append(record) + + def _is_logged(self, log_level: str, message: str) -> bool: + return any(record.levelname == log_level and record.getMessage() == message + for record in self.log_records) + + def _create_exception_and_unlock_test_with_event(self, exception): + self.resume_test_event.set() + raise exception def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret - def get_last_traded_prices_mock(self, last_trade_close: Decimal) -> Dict: - last_traded_prices = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "a": [ - "52609.60000", - "1", - "1.000" - ], - "b": [ - "52609.50000", - "1", - "1.000" - ], - "c": [ - str(last_trade_close), - "0.00080000" - ], - "v": [ - "1920.83610601", - "7954.00219674" - ], - "p": [ - "52389.94668", - "54022.90683" - ], - "t": [ - 23329, - 80463 - ], - "l": [ - "51513.90000", - "51513.90000" - ], - "h": [ - "53219.90000", - "57200.00000" + def _trade_update_event(self): + resp = [ + 0, + [ + [ + "5541.20000", + "0.15850568", + "1534614057.321597", + "s", + "l", + "" + ] + ], + "trade", + f"{self.base_asset}/{self.quote_asset}" + ] + return resp + + def _order_diff_event(self): + resp = [ + 1234, + { + "a": [ + [ + "5541.30000", + "2.50700000", + "1534614248.456738" ], - "o": "52280.40000" - } - } - } - return last_traded_prices + [ + "5542.50000", + "0.40100000", + "1534614248.456738" + ] + ], + "c": "974942666" + }, + "book-10", + "XBT/USD" + ] + return resp - def get_depth_mock(self) -> Dict: - depth = { + def _snapshot_response(self): + resp = { "error": [], "result": { f"X{self.base_asset}{self.quote_asset}": { @@ -114,113 +151,16 @@ def get_depth_mock(self) -> Dict: } } } - return depth - - def get_public_asset_pair_mock(self) -> Dict: - asset_pairs = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "altname": f"{self.base_asset}{self.quote_asset}", - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 5, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [ - 2, - 3, - 4, - 5 - ], - "leverage_sell": [ - 2, - 3, - 4, - 5 - ], - "fees": [ - [ - 0, - 0.26 - ], - [ - 50000, - 0.24 - ], - ], - "fees_maker": [ - [ - 0, - 0.16 - ], - [ - 50000, - 0.14 - ], - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.005" - }, - } - } - return asset_pairs - - def get_trade_data_mock(self) -> List: - trade_data = [ - 0, - [ - [ - "5541.20000", - "0.15850568", - "1534614057.321597", - "s", - "l", - "" - ], - [ - "6060.00000", - "0.02455000", - "1534614057.324998", - "b", - "l", - "" - ] - ], - "trade", - f"{self.base_asset}/{self.quote_asset}" - ] - return trade_data + return resp @aioresponses() - def test_get_last_traded_prices(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" + def test_get_new_order_book_successful(self, mock_api): + url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - last_traded_price = Decimal("52641.10000") - resp = self.get_last_traded_prices_mock(last_trade_close=last_traded_price) - mocked_api.get(regex_url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout( - KrakenAPIOrderBookDataSource.get_last_traded_prices( - trading_pairs=[self.trading_pair], throttler=self.throttler - ) - ) - self.assertIn(self.trading_pair, ret) - self.assertEqual(float(last_traded_price), ret[self.trading_pair]) + resp = self._snapshot_response() - @aioresponses() - def test_get_new_order_book(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_depth_mock() - mocked_api.get(regex_url, body=json.dumps(resp)) + mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair)) @@ -234,38 +174,254 @@ def test_get_new_order_book(self, mocked_api): self.assertEqual(first_bid_price, bids_df.iloc[0]["price"]) self.assertEqual(first_ask_price, asks_df.iloc[0]["price"]) - # @aioresponses() - # def test_fetch_trading_pairs(self, mocked_api): - # url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - # regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - # resp = self.get_public_asset_pair_mock() - # mocked_api.get(regex_url, body=json.dumps(resp)) - # - # resp = self.async_run_with_timeout(KrakenAPIOrderBookDataSource.fetch_trading_pairs(), 2) - # - # self.assertTrue(len(resp) == 1) - # self.assertIn(self.trading_pair, resp) + @aioresponses() + def test_get_new_order_book_raises_exception(self, mock_api): + url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + mock_api.get(regex_url, status=400) + with self.assertRaises(IOError): + self.async_run_with_timeout( + self.data_source.get_new_order_book(self.trading_pair) + ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) - def test_listen_for_trades(self, ws_connect_mock): + def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() - resp = self.get_trade_data_mock() + + result_subscribe_trades = { + "code": None, + "id": 1 + } + result_subscribe_diffs = { + "code": None, + "id": 2 + } + self.mocking_assistant.add_websocket_aiohttp_message( - websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) + websocket_mock=ws_connect_mock.return_value, + message=json.dumps(result_subscribe_trades)) + self.mocking_assistant.add_websocket_aiohttp_message( + websocket_mock=ws_connect_mock.return_value, + message=json.dumps(result_subscribe_diffs)) + + self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) + + self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) + + sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( + websocket_mock=ws_connect_mock.return_value) + + self.assertEqual(2, len(sent_subscription_messages)) + expected_trade_subscription = { + "event": "subscribe", + "pair": [self.ws_ex_trading_pairs], + "subscription": {"name": 'trade', "depth": 1000}, + } + self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) + expected_diff_subscription = { + "event": "subscribe", + "pair": [self.ws_ex_trading_pairs], + "subscription": {"name": 'book', "depth": 1000}, + } + self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) + + self.assertTrue(self._is_logged( + "INFO", + "Subscribed to public order book and trade channels..." + )) + + @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep") + @patch("aiohttp.ClientSession.ws_connect") + def test_listen_for_subscriptions_raises_cancel_exception(self, mock_ws, _: AsyncMock): + mock_ws.side_effect = asyncio.CancelledError + + with self.assertRaises(asyncio.CancelledError): + self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) + self.async_run_with_timeout(self.listening_task) + + @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep") + @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) + def test_listen_for_subscriptions_logs_exception_details(self, mock_ws, sleep_mock): + mock_ws.side_effect = Exception("TEST ERROR.") + sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError()) + + self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) + + self.async_run_with_timeout(self.resume_test_event.wait()) + + self.assertTrue( + self._is_logged( + "ERROR", + "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds...")) + + def test_subscribe_channels_raises_cancel_exception(self): + mock_ws = MagicMock() + mock_ws.send.side_effect = asyncio.CancelledError + + with self.assertRaises(asyncio.CancelledError): + self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws)) + self.async_run_with_timeout(self.listening_task) + + def test_subscribe_channels_raises_exception_and_logs_error(self): + mock_ws = MagicMock() + mock_ws.send.side_effect = Exception("Test Error") + + with self.assertRaises(Exception): + self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws)) + self.async_run_with_timeout(self.listening_task) + + self.assertTrue( + self._is_logged("ERROR", "Unexpected error occurred subscribing to order book trading and delta streams...") + ) + + def test_listen_for_trades_cancelled_when_listening(self): + mock_queue = MagicMock() + mock_queue.get.side_effect = asyncio.CancelledError() + self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + with self.assertRaises(asyncio.CancelledError): + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_trades(self.ev_loop, msg_queue) + ) + self.async_run_with_timeout(self.listening_task) + + def test_listen_for_trades_logs_exception(self): + incomplete_resp = { + "m": 1, + "i": 2, + } + + mock_queue = AsyncMock() + mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] + self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_trades(self.ev_loop, msg_queue) + ) + + try: + self.async_run_with_timeout(self.listening_task) + except asyncio.CancelledError: + pass + + self.assertTrue( + self._is_logged("ERROR", "Unexpected error when processing public trade updates from exchange")) + + def test_listen_for_trades_successful(self): + mock_queue = AsyncMock() + mock_queue.get.side_effect = [self._trade_update_event(), asyncio.CancelledError()] + self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_trades(self.ev_loop, msg_queue)) + + msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) + + self.assertEqual(1534614057.324998, msg.trade_id) + + def test_listen_for_order_book_diffs_cancelled(self): + mock_queue = AsyncMock() + mock_queue.get.side_effect = asyncio.CancelledError() + self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + with self.assertRaises(asyncio.CancelledError): + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) + ) + self.async_run_with_timeout(self.listening_task) + + def test_listen_for_order_book_diffs_logs_exception(self): + incomplete_resp = { + "m": 1, + "i": 2, + } + + mock_queue = AsyncMock() + mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] + self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) ) - output_queue = asyncio.Queue() - - self.ev_loop.create_task(self.data_source.listen_for_trades(self.ev_loop, output_queue)) - self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value) - - self.assertTrue(not output_queue.empty()) - msg = output_queue.get_nowait() - self.assertTrue(isinstance(msg, OrderBookMessage)) - first_trade_price = resp[1][0][0] - self.assertEqual(msg.content["price"], first_trade_price) - - self.assertTrue(not output_queue.empty()) - msg = output_queue.get_nowait() - self.assertTrue(isinstance(msg, OrderBookMessage)) - second_trade_price = resp[1][1][0] - self.assertEqual(msg.content["price"], second_trade_price) + + try: + self.async_run_with_timeout(self.listening_task) + except asyncio.CancelledError: + pass + + self.assertTrue( + self._is_logged("ERROR", "Unexpected error when processing public order book updates from exchange")) + + def test_listen_for_order_book_diffs_successful(self): + mock_queue = AsyncMock() + diff_event = self._order_diff_event() + mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] + self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + + msg_queue: asyncio.Queue = asyncio.Queue() + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)) + + msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) + + self.assertEqual(int(diff_event[1]["a"][0][0][2]), msg.update_id) + + @aioresponses() + def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(self, mock_api): + url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + mock_api.get(regex_url, exception=asyncio.CancelledError, repeat=True) + + with self.assertRaises(asyncio.CancelledError): + self.async_run_with_timeout( + self.data_source.listen_for_order_book_snapshots(self.ev_loop, asyncio.Queue()) + ) + + @aioresponses() + @patch("hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source" + ".KrakenAPIOrderBookDataSource._sleep") + def test_listen_for_order_book_snapshots_log_exception(self, mock_api, sleep_mock): + msg_queue: asyncio.Queue = asyncio.Queue() + sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError()) + + url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + mock_api.get(regex_url, exception=Exception, repeat=True) + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue) + ) + self.async_run_with_timeout(self.resume_test_event.wait()) + + self.assertTrue( + self._is_logged("ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}.")) + + @aioresponses() + def test_listen_for_order_book_snapshots_successful(self, mock_api, ): + msg_queue: asyncio.Queue = asyncio.Queue() + url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + + mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) + + self.listening_task = self.ev_loop.create_task( + self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue) + ) + + msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) + + self.assertEqual(1616663113, msg.update_id) From 3c9ceaf923598661577835b9ea52882bdf54d89d Mon Sep 17 00:00:00 2001 From: bczhang Date: Sat, 17 Feb 2024 01:50:56 +0800 Subject: [PATCH 11/34] add all unittest --- .../exchange/kraken/kraken_exchange.py | 4 +- ...test_kraken_api_user_stream_data_source.py | 40 +- .../exchange/kraken/test_kraken_auth.py | 64 + .../exchange/kraken/test_kraken_exchange.py | 1173 +++++++---------- 4 files changed, 599 insertions(+), 682 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index c023edaf30..4c4ec45a9d 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -59,8 +59,6 @@ def __init__(self, self._domain = domain self._trading_required = trading_required self._trading_pairs = trading_pairs - # todo - self._last_trades_poll_kraken_timestamp = 1.0 self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) self._asset_pairs = {} @@ -70,7 +68,7 @@ def __init__(self, @staticmethod def kraken_order_type(order_type: OrderType) -> str: - return order_type.name.upper() + return order_type.name.lower() @staticmethod def to_hb_order_type(kraken_type: str) -> OrderType: diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index 7786f2ffa6..ffc063e4ab 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -2,12 +2,16 @@ import json import re import unittest -from typing import Awaitable, Dict, List -from unittest.mock import AsyncMock, patch +from typing import Awaitable, Dict, List, Optional +from unittest.mock import AsyncMock, patch, MagicMock from aioresponses import aioresponses +from bidict import bidict +from hummingbot.client.config.client_config_map import ClientConfigMap +from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS +from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier @@ -24,17 +28,43 @@ def setUpClass(cls) -> None: cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}" + cls.ws_ex_trading_pair = cls.base_asset + "/" + cls.quote_asset cls.api_tier = KrakenAPITier.STARTER def setUp(self) -> None: super().setUp() + self.log_records = [] + self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() + self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) + self.mock_time_provider = MagicMock() + + client_config_map = ClientConfigAdapter(ClientConfigMap()) + self.connector = KrakenExchange( + client_config_map=client_config_map, + kraken_api_key="", + kraken_secret_key="", + trading_pairs=[], + trading_required=False) + not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" - kraken_auth = KrakenAuth(api_key="someKey", secret_key=not_a_real_secret) - self.data_source = KrakenAPIUserStreamDataSource(self.throttler, kraken_auth) + self.auth = KrakenAuth(api_key="someKey", secret_key=not_a_real_secret, time_provider=self.mock_time_provider) + + self.connector._web_assistants_factory._auth = self.auth + self.data_source = KrakenAPIUserStreamDataSource(self.connector, + api_factory=self.connector._web_assistants_factory, + ) + + self.data_source.logger().setLevel(1) + self.data_source.logger().addHandler(self) + + self.resume_test_event = asyncio.Event() + + self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair})) - def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): + def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py index e69de29bb2..cdad78296a 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py @@ -0,0 +1,64 @@ +import asyncio +import base64 +import hashlib +import hmac +from copy import copy +from unittest import TestCase +from unittest.mock import MagicMock + +from typing_extensions import Awaitable + +from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest + + +class KrakenAuthTests(TestCase): + + def setUp(self) -> None: + self._api_key = "testApiKey" + self._secret = "testSecret" + + def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): + ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout)) + return ret + + def test_rest_authenticate(self): + now = 1234567890.000 + mock_time_provider = MagicMock() + mock_time_provider.time.return_value = now + test_url = "/test" + params = { + "symbol": "LTCBTC", + "side": "BUY", + "type": "LIMIT", + "timeInForce": "GTC", + "quantity": 1, + "price": "0.1", + } + full_params = copy(params) + + auth = KrakenAuth(api_key=self._api_key, secret_key=self._secret, time_provider=mock_time_provider) + request = RESTRequest(method=RESTMethod.GET, params=params, is_auth_required=True) + request.url = test_url + configured_request = self.async_run_with_timeout(auth.rest_authenticate(request)) + + # full_params.update({"timestamp": 1234567890000}) + + api_path: bytes = bytes(request.url, 'utf-8') + api_nonce: str = "1234567890000" + api_post: str = "nonce=" + api_nonce + + for key, value in params.items(): + api_post += f"&{key}={value}" + + api_sha256: bytes = hashlib.sha256(bytes(api_nonce + api_post, 'utf-8')).digest() + api_hmac: hmac.HMAC = hmac.new(self._secret.encode("utf-8"), api_path + api_sha256, hashlib.sha512) + expected_signature: bytes = base64.b64encode(api_hmac.digest()) + # + # expected_signature = hmac.new( + # self._secret.encode("utf-8"), + # encoded_params.encode("utf-8"), + # hashlib.sha256).hexdigest() + # self.assertEqual(now * 1e3, configured_request.params["timestamp"]) + self.assertEqual(str(expected_signature, 'utf-8'), configured_request.headers["signature"]["API-Sign"]) + self.assertEqual(self._api_key, configured_request.headers["API-Key"]) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index fb7d74dc01..f6898e08bf 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -12,6 +12,9 @@ from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange +from hummingbot.connector.exchange.kraken.kraken_utils import ( + convert_to_exchange_trading_pair, +) from hummingbot.connector.test_support.exchange_connector_test import AbstractExchangeConnectorTests from hummingbot.connector.trading_rule import TradingRule from hummingbot.connector.utils import get_new_client_order_id @@ -25,168 +28,162 @@ class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests) @property def all_symbols_url(self): - return web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.exchange._domain) + return web_utils.public_rest_url(path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) @property def latest_prices_url(self): - url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.exchange._domain) + url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_PATH_URL) url = f"{url}?symbol={self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset)}" return url @property def network_status_url(self): - url = web_utils.private_rest_url(CONSTANTS.PING_PATH_URL, domain=self.exchange._domain) + url = web_utils.private_rest_url(CONSTANTS.TICKER_PATH_URL) return url @property def trading_rules_url(self): - url = web_utils.private_rest_url(CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.exchange._domain) + url = web_utils.private_rest_url(CONSTANTS.ASSET_PAIRS_PATH_URL) return url @property def order_creation_url(self): - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL, domain=self.exchange._domain) + url = web_utils.private_rest_url(CONSTANTS.ADD_ORDER_PATH_URL) return url @property def balance_url(self): - url = web_utils.private_rest_url(CONSTANTS.ACCOUNTS_PATH_URL, domain=self.exchange._domain) + url = web_utils.private_rest_url(CONSTANTS.BALANCE_PATH_URL) return url @property def all_symbols_request_mock_response(self): return { - "timezone": "UTC", - "serverTime": 1639598493658, - "rateLimits": [], - "exchangeFilters": [], - "symbols": [ - { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "status": "ENABLED", - "baseAsset": self.base_asset, - "baseSizePrecision": 1e-8, - "quotePrecision": 8, - "baseAssetPrecision": 8, - "quoteAmountPrecision": 8, - "quoteAsset": self.quote_asset, - "quoteAssetPrecision": 8, - "baseCommissionPrecision": 8, - "quoteCommissionPrecision": 8, - "orderTypes": [ - "LIMIT", - "LIMIT_MAKER", - "MARKET", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT_LIMIT" - ], - "icebergAllowed": True, - "ocoAllowed": True, - "quoteOrderQtyMarketAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - "filters": [], - "permissions": [ - "SPOT", - "MARGIN" - ] - }, - ] - } - - @property - def latest_prices_request_mock_response(self): - return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "priceChange": "-94.99999800", - "priceChangePercent": "-95.960", - "weightedAvgPrice": "0.29628482", - "prevClosePrice": "0.10002000", - "lastPrice": str(self.expected_latest_price), - "lastQty": "200.00000000", - "bidPrice": "4.00000000", - "bidQty": "100.00000000", - "askPrice": "4.00000200", - "askQty": "100.00000000", - "openPrice": "99.00000000", - "highPrice": "100.00000000", - "lowPrice": "0.10000000", - "volume": "8913.30000000", - "quoteVolume": "15.30000000", - "openTime": 1499783499040, - "closeTime": 1499869899040, - "firstId": 28385, - "lastId": 28460, - "count": 76, + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: response = { - "timezone": "UTC", - "serverTime": 1639598493658, - "rateLimits": [], - "exchangeFilters": [], - "symbols": [ - { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "status": "ENABLED", - "baseAsset": self.base_asset, - "baseSizePrecision": 1e-8, - "quotePrecision": 8, - "baseAssetPrecision": 8, - "quoteAsset": self.quote_asset, - "quoteAssetPrecision": 8, - "baseCommissionPrecision": 8, - "quoteAmountPrecision": 8, - "quoteCommissionPrecision": 8, - "orderTypes": [ - "LIMIT", - "LIMIT_MAKER", - "MARKET", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT_LIMIT" - ], - "icebergAllowed": True, - "ocoAllowed": True, - "quoteOrderQtyMarketAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - "filters": [], - "permissions": [ - "MARGIN" - ] - }, - { - "symbol": self.exchange_symbol_for_tokens("INVALID", "PAIR"), - "status": "ENABLED", - "baseAsset": "INVALID", - "baseSizePrecision": 1e-8, - "quotePrecision": 8, - "baseAssetPrecision": 8, - "quoteAmountPrecision": 8, - "quoteAsset": "PAIR", - "quoteAssetPrecision": 8, - "baseCommissionPrecision": 8, - "quoteCommissionPrecision": 8, - "orderTypes": [ - "LIMIT", - "LIMIT_MAKER", - "MARKET", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT_LIMIT" - ], - "icebergAllowed": True, - "ocoAllowed": True, - "quoteOrderQtyMarketAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - "filters": [], - "permissions": [ - "MARGIN" - ] - }, - ] + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + }, + "ETHUSDT.d": { + "altname": "ETHUSDT.d", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } return "INVALID-PAIR", response @@ -198,149 +195,120 @@ def network_status_request_successful_mock_response(self): @property def trading_rules_request_mock_response(self): return { - "timezone": "UTC", - "serverTime": 1565246363776, - "rateLimits": [{}], - "exchangeFilters": [], - "symbols": [ - { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "status": "ENABLED", - "baseAsset": self.base_asset, - "baseSizePrecision": 1e-8, - "quotePrecision": 8, - "baseAssetPrecision": 8, - "quoteAmountPrecision": 8, - "quoteAsset": self.quote_asset, - "quoteAssetPrecision": 8, - "orderTypes": ["LIMIT", "LIMIT_MAKER"], - "icebergAllowed": True, - "ocoAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - - "filters": [ - { - "filterType": "PRICE_FILTER", - "minPrice": "0.00000100", - "maxPrice": "100000.00000000", - "tickSize": "0.00000100" - }, { - "filterType": "LOT_SIZE", - "minQty": "0.00100000", - "maxQty": "200000.00000000", - "stepSize": "0.00100000" - }, { - "filterType": "MIN_NOTIONAL", - "minNotional": "0.00200000" - } - ], - "permissions": [ - "SPOT", - "MARGIN" - ] - } - ] + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } @property def trading_rules_request_erroneous_mock_response(self): return { - "timezone": "UTC", - "serverTime": 1565246363776, - "rateLimits": [{}], - "exchangeFilters": [], - "symbols": [ - { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "status": "ENABLED", - "baseAsset": self.base_asset, - "baseAssetPrecision": 8, - "quoteAsset": self.quote_asset, - "quotePrecision": 8, - "quoteAssetPrecision": 8, - "orderTypes": ["LIMIT", "LIMIT_MAKER"], - "icebergAllowed": True, - "ocoAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - "permissions": [ - "SPOT", - "MARGIN" - ] - } - ] + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + } } @property def order_creation_request_successful_mock_response(self): return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": self.expected_exchange_order_id, - "orderListId": -1, - "clientOrderId": "OID1", - "transactTime": 1507725176595 + "error": [], + "result": { + "descr": { + "order": "", + }, + "txid": [ + self.expected_exchange_order_id, + ] + } } @property def balance_request_mock_response_for_base_and_quote(self): return { - "makerCommission": 15, - "takerCommission": 15, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": True, - "canWithdraw": True, - "canDeposit": True, - "updateTime": 123456789, - "accountType": "SPOT", - "balances": [ - { - "asset": self.base_asset, - "free": "10.0", - "locked": "5.0" - }, - { - "asset": self.quote_asset, - "free": "2000", - "locked": "0.00000000" - } - ], - "permissions": [ - "SPOT" - ] + "error": [], + "result": { + self.base_asset: str(10), + self.quote_asset: str(2000), + } } @property def balance_request_mock_response_only_base(self): return { - "makerCommission": 15, - "takerCommission": 15, - "buyerCommission": 0, - "sellerCommission": 0, - "canTrade": True, - "canWithdraw": True, - "canDeposit": True, - "updateTime": 123456789, - "accountType": "SPOT", - "balances": [{"asset": self.base_asset, "free": "10.0", "locked": "5.0"}], - "permissions": ["SPOT"], - } - - @property - def balance_event_websocket_update(self): - return { - "c": "spot@private.account.v3.api", - "d": { - "a": self.base_asset, - "c": 1564034571105, - "f": "10", - "fd": "-4.990689704", - "l": "5", - "ld": "4.990689704", - "o": "ENTRUST_PLACE" - }, - "t": 1564034571073 + "error": [], + "result": { + self.base_asset: str(10), + } } @property @@ -353,14 +321,15 @@ def expected_supported_order_types(self): @property def expected_trading_rule(self): + rule = list(self.trading_rules_request_mock_response.values())[0] + min_order_size = Decimal(rule.get('ordermin', 0)) + min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") + min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") return TradingRule( trading_pair=self.trading_pair, - min_order_size=Decimal(self.trading_rules_request_mock_response["symbols"][0]["baseSizePrecision"]), - min_price_increment=Decimal( - f'1e-{self.trading_rules_request_mock_response["symbols"][0]["quotePrecision"]}'), - min_base_amount_increment=Decimal( - f'1e-{self.trading_rules_request_mock_response["symbols"][0]["baseAssetPrecision"]}'), - min_notional_size=Decimal(self.trading_rules_request_mock_response["symbols"][0]["quoteAmountPrecision"]), + min_order_size=min_order_size, + min_price_increment=min_price_increment, + min_base_amount_increment=min_base_amount_increment, ) @property @@ -406,49 +375,42 @@ def create_exchange_instance(self): return KrakenExchange( client_config_map=client_config_map, kraken_api_key="testAPIKey", - kraken_api_secret="testSecret", + kraken_secret_key="testSecret", trading_pairs=[self.trading_pair], ) def validate_auth_credentials_present(self, request_call: RequestCall): self._validate_auth_credentials_taking_parameters_from_argument( request_call_tuple=request_call, - params=request_call.kwargs["params"] or request_call.kwargs["data"] + params=request_call.kwargs["data"] ) def validate_order_creation_request(self, order: InFlightOrder, request_call: RequestCall): request_data = dict(request_call.kwargs["data"]) - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_data["symbol"]) - self.assertEqual(order.trade_type.name.upper(), request_data["side"]) - self.assertEqual(KrakenExchange.kraken_order_type(OrderType.LIMIT), request_data["type"]) - self.assertEqual(Decimal("100"), Decimal(request_data["quantity"])) + self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_data["pair"]) + self.assertEqual(order.trade_type.name.upper(), request_data["type"]) + self.assertEqual(KrakenExchange.kraken_order_type(OrderType.LIMIT), request_data["ordertype"]) + self.assertEqual(Decimal("100"), Decimal(request_data["volume"])) self.assertEqual(Decimal("10000"), Decimal(request_data["price"])) - self.assertEqual(order.client_order_id, request_data["newClientOrderId"]) def validate_order_cancelation_request(self, order: InFlightOrder, request_call: RequestCall): - request_data = dict(request_call.kwargs["params"]) - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - request_data["symbol"]) - self.assertEqual(order.client_order_id, request_data["origClientOrderId"]) + request_data = dict(request_call.kwargs["data"]) + self.assertEqual(order.exchange_order_id, request_data["txid"]) def validate_order_status_request(self, order: InFlightOrder, request_call: RequestCall): request_params = request_call.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - request_params["symbol"]) - self.assertEqual(order.client_order_id, request_params["origClientOrderId"]) + self.assertEqual(order.exchange_order_id, request_params["txid"]) def validate_trades_request(self, order: InFlightOrder, request_call: RequestCall): - request_params = request_call.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - request_params["symbol"]) - self.assertEqual(order.exchange_order_id, str(request_params["orderId"])) + request_params = request_call.kwargs["data"] + self.assertEqual(order.exchange_order_id, str(request_params["txid"])) def configure_successful_cancelation_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_cancelation_request_successful_mock_response(order=order) mock_api.delete(regex_url, body=json.dumps(response), callback=callback) @@ -459,7 +421,7 @@ def configure_erroneous_cancelation_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.delete(regex_url, status=400, callback=callback) return url @@ -468,9 +430,13 @@ def configure_order_not_found_error_cancelation_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - response = {"code": -2011, "msg": "Unknown order sent."} + response = { + "error": [ + "API key doesn't have permission to make this request" + ] + } mock_api.delete(regex_url, status=400, body=json.dumps(response), callback=callback) return url @@ -494,7 +460,7 @@ def configure_completely_filled_order_status_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_completely_filled_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -505,7 +471,7 @@ def configure_canceled_order_status_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_canceled_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -516,7 +482,7 @@ def configure_erroneous_http_fill_trade_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") mock_api.get(regex_url, status=400, callback=callback) return url @@ -529,7 +495,7 @@ def configure_open_order_status_response( """ :return: the URL configured """ - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_open_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -540,7 +506,7 @@ def configure_http_error_order_status_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=401, callback=callback) return url @@ -550,7 +516,7 @@ def configure_partially_filled_order_status_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_partially_filled_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -560,7 +526,7 @@ def configure_order_not_found_error_order_status_response( self, order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None ) -> List[str]: - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = {"code": -2013, "msg": "Order does not exist."} mock_api.get(regex_url, body=json.dumps(response), status=400, callback=callback) @@ -571,7 +537,7 @@ def configure_partial_fill_trade_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") response = self._order_fills_request_partial_fill_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -582,35 +548,50 @@ def configure_full_fill_trade_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(path_url=CONSTANTS.MY_TRADES_PATH_URL) + url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") response = self._order_fills_request_full_fill_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) return url def order_event_for_new_order_websocket_update(self, order: InFlightOrder): - return { - "c": "spot@private.orders.v3.api", - "d": { - "A": 8.0, - "O": 1661938138000, - "S": 1, - "V": 10, - "a": 8, - "c": order.client_order_id, - "i": order.exchange_order_id, - "m": 0, - "o": 1, - "p": order.price, - "s": 1, - "v": order.amount, - "ap": 0, - "cv": 0, - "ca": 0 - }, - "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "t": 1499405658657 - } + return [ + [ + { + order.exchange_order_id: { + "avg_price": "34.50000", + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", + "ordertype": "limit", + "pair": convert_to_exchange_trading_pair(self.trading_pair, '/'), + "price": str(order.price), + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "34.50000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": str(order.amount,), + "vol_exec": "0.00000000" + } + } + ], + "openOrders", + { + "sequence": 234 + } + ] def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): return { @@ -684,51 +665,15 @@ def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): @aioresponses() @patch("hummingbot.connector.time_synchronizer.TimeSynchronizer._current_seconds_counter") def test_update_time_synchronizer_successfully(self, mock_api, seconds_counter_mock): - request_sent_event = asyncio.Event() - seconds_counter_mock.side_effect = [0, 0, 0] - - self.exchange._time_synchronizer.clear_time_offset_ms_samples() - url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - response = {"serverTime": 1640000003000} - - mock_api.get(regex_url, - body=json.dumps(response), - callback=lambda *args, **kwargs: request_sent_event.set()) - - self.async_run_with_timeout(self.exchange._update_time_synchronizer()) - - self.assertEqual(response["serverTime"] * 1e-3, self.exchange._time_synchronizer.time()) + pass @aioresponses() def test_update_time_synchronizer_failure_is_logged(self, mock_api): - request_sent_event = asyncio.Event() - - url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - response = {"code": -1121, "msg": "Dummy error"} - - mock_api.get(regex_url, - body=json.dumps(response), - callback=lambda *args, **kwargs: request_sent_event.set()) - - self.async_run_with_timeout(self.exchange._update_time_synchronizer()) - - self.assertTrue(self.is_logged("NETWORK", "Error getting server time.")) + pass @aioresponses() def test_update_time_synchronizer_raises_cancelled_error(self, mock_api): - url = web_utils.private_rest_url(CONSTANTS.SERVER_TIME_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - mock_api.get(regex_url, - exception=asyncio.CancelledError) - - self.assertRaises( - asyncio.CancelledError, - self.async_run_with_timeout, self.exchange._update_time_synchronizer()) + pass @aioresponses() def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): @@ -747,39 +692,51 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): ) order = self.exchange.in_flight_orders["OID1"] - url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) trade_fill = { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "id": 28457, - "orderId": int(order.exchange_order_id), - "orderListId": -1, - "price": "9999", - "qty": "1", - "quoteQty": "48.000012", - "commission": "10.10000000", - "commissionAsset": self.quote_asset, - "time": 1499865549590, - "isBuyer": True, - "isMaker": False, - "isBestMatch": True + "error": [], + "result": { + 28457: { + "ordertxid": order.exchange_order_id, + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(self.expected_partial_fill_price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(self.expected_partial_fill_amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" + } + } } trade_fill_non_tracked_order = { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "id": 30000, - "orderId": 99999, - "orderListId": -1, - "price": "4.00000100", - "qty": "12.00000000", - "quoteQty": "48.000012", - "commission": "10.10000000", - "commissionAsset": "BNB", - "time": 1499865549590, - "isBuyer": True, - "isMaker": False, - "isBestMatch": True + "error": [], + "result": { + 30000: { + "ordertxid": 9999, + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(self.expected_partial_fill_price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(self.expected_partial_fill_amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" + } + } } mock_response = [trade_fill, trade_fill_non_tracked_order] @@ -792,8 +749,6 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): request = self._all_executed_requests(mock_api, url)[0] self.validate_auth_credentials_present(request) - request_params = request.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] self.assertEqual(self.exchange.current_timestamp, fill_event.timestamp) @@ -802,9 +757,9 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): self.assertEqual(order.trade_type, fill_event.trade_type) self.assertEqual(order.order_type, fill_event.order_type) self.assertEqual(Decimal(trade_fill["price"]), fill_event.price) - self.assertEqual(Decimal(trade_fill["qty"]), fill_event.amount) + self.assertEqual(Decimal(trade_fill["vol"]), fill_event.amount) self.assertEqual(0.0, fill_event.trade_fee.percent) - self.assertEqual([TokenAmount(trade_fill["commissionAsset"], Decimal(trade_fill["commission"]))], + self.assertEqual([TokenAmount(self.quote_asset, Decimal(trade_fill["fee"]))], fill_event.trade_fee.flat_fees) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1] @@ -814,105 +769,18 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): self.assertEqual(TradeType.BUY, fill_event.trade_type) self.assertEqual(OrderType.LIMIT, fill_event.order_type) self.assertEqual(Decimal(trade_fill_non_tracked_order["price"]), fill_event.price) - self.assertEqual(Decimal(trade_fill_non_tracked_order["qty"]), fill_event.amount) + self.assertEqual(Decimal(trade_fill_non_tracked_order["vol"]), fill_event.amount) self.assertEqual(0.0, fill_event.trade_fee.percent) self.assertEqual([ TokenAmount( - trade_fill_non_tracked_order["commissionAsset"], - Decimal(trade_fill_non_tracked_order["commission"]))], + self.quote_asset, + Decimal(trade_fill_non_tracked_order["fee"]))], fill_event.trade_fee.flat_fees) self.assertTrue(self.is_logged( "INFO", f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" )) - @aioresponses() - def test_update_order_fills_request_parameters(self, mock_api): - self.exchange._set_current_timestamp(0) - self.exchange._last_poll_timestamp = -1 - - url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - mock_response = [] - mock_api.get(regex_url, body=json.dumps(mock_response)) - - self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - - request = self._all_executed_requests(mock_api, url)[0] - self.validate_auth_credentials_present(request) - request_params = request.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) - self.assertNotIn("startTime", request_params) - - self.exchange._set_current_timestamp(1640780000) - self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - self.exchange._last_trades_poll_kraken_timestamp = 10 - self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - - request = self._all_executed_requests(mock_api, url)[1] - self.validate_auth_credentials_present(request) - request_params = request.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) - self.assertEqual(10 * 1e3, request_params["startTime"]) - - @aioresponses() - def test_update_order_fills_from_trades_with_repeated_fill_triggers_only_one_event(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - - url = web_utils.private_rest_url(CONSTANTS.MY_TRADES_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - trade_fill_non_tracked_order = { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "id": 30000, - "orderId": 99999, - "orderListId": -1, - "price": "4.00000100", - "qty": "12.00000000", - "quoteQty": "48.000012", - "commission": "10.10000000", - "commissionAsset": "BNB", - "time": 1499865549590, - "isBuyer": True, - "isMaker": False, - "isBestMatch": True - } - - mock_response = [trade_fill_non_tracked_order, trade_fill_non_tracked_order] - mock_api.get(regex_url, body=json.dumps(mock_response)) - - self.exchange.add_exchange_order_ids_from_market_recorder( - {str(trade_fill_non_tracked_order["orderId"]): "OID99"}) - - self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - - request = self._all_executed_requests(mock_api, url)[0] - self.validate_auth_credentials_present(request) - request_params = request.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) - - self.assertEqual(1, len(self.order_filled_logger.event_log)) - fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] - self.assertEqual(float(trade_fill_non_tracked_order["time"]) * 1e-3, fill_event.timestamp) - self.assertEqual("OID99", fill_event.order_id) - self.assertEqual(self.trading_pair, fill_event.trading_pair) - self.assertEqual(TradeType.BUY, fill_event.trade_type) - self.assertEqual(OrderType.LIMIT, fill_event.order_type) - self.assertEqual(Decimal(trade_fill_non_tracked_order["price"]), fill_event.price) - self.assertEqual(Decimal(trade_fill_non_tracked_order["qty"]), fill_event.amount) - self.assertEqual(0.0, fill_event.trade_fee.percent) - self.assertEqual([ - TokenAmount(trade_fill_non_tracked_order["commissionAsset"], - Decimal(trade_fill_non_tracked_order["commission"]))], - fill_event.trade_fee.flat_fees) - self.assertTrue(self.is_logged( - "INFO", - f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" - )) @aioresponses() def test_update_order_status_when_failed(self, mock_api): @@ -931,30 +799,37 @@ def test_update_order_status_when_failed(self, mock_api): ) order = self.exchange.in_flight_orders["OID1"] - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) order_status = { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": int(order.exchange_order_id), - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": "10000.0", - "origQty": "1.0", - "executedQty": "0.0", - "cummulativeQuoteQty": "0.0", - "status": "REJECTED", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "BUY", - "stopPrice": "0.0", - "icebergQty": "0.0", - "time": 1499827319559, - "updateTime": 1499827319559, - "isWorking": True, - "origQuoteOrderQty": "10000.000000" + "error": [], + "result": { + "open": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1499827319.559, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": "1.0", + "vol_exec": "0.0", + "cost": "11253.7", + "fee": "0.00000", + "price": "10000.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } + } + } } + mock_response = order_status mock_api.get(regex_url, body=json.dumps(mock_response)) @@ -963,8 +838,7 @@ def test_update_order_status_when_failed(self, mock_api): request = self._all_executed_requests(mock_api, url)[0] self.validate_auth_credentials_present(request) request_params = request.kwargs["params"] - self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_params["symbol"]) - self.assertEqual(order.client_order_id, request_params["origClientOrderId"]) + self.assertEqual(order.exchange_order_id, request_params["txid"]) failure_event: MarketOrderFailureEvent = self.order_failure_logger.event_log[0] self.assertEqual(self.exchange.current_timestamp, failure_event.timestamp) @@ -1014,31 +888,12 @@ def test_client_order_id_on_order(self, mocked_nonce): self.assertEqual(result, expected_client_order_id) - def test_time_synchronizer_related_request_error_detection(self): - exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " - "Error: {'code':-1021,'msg':'Timestamp for this request is outside of the recvWindow.'}") - self.assertTrue(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) - - exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " - "Error: {'code':-1021,'msg':'Timestamp for this request was 1000ms ahead of the server's " - "time.'}") - self.assertTrue(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) - - exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " - "Error: {'code':-1022,'msg':'Timestamp for this request was 1000ms ahead of the server's " - "time.'}") - self.assertFalse(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) - - exception = IOError("Error executing request POST https://api.kraken.com/api/v3/order. HTTP status is 400. " - "Error: {'code':-1021,'msg':'Other error.'}") - self.assertFalse(self.exchange._is_request_exception_related_to_time_synchronizer(exception)) - @aioresponses() def test_place_order_manage_server_overloaded_error_unkown_order(self, mock_api): self.exchange._set_current_timestamp(1640780000) self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.ADD_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"code": -1003, "msg": "Unknown error, please check your request or try again later."} mock_api.post(regex_url, body=json.dumps(mock_response), status=503) @@ -1059,7 +914,7 @@ def test_place_order_manage_server_overloaded_error_failure(self, mock_api): self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - url = web_utils.private_rest_url(CONSTANTS.ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.ADD_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"code": -1003, "msg": "Service Unavailable."} mock_api.post(regex_url, body=json.dumps(mock_response), status=503) @@ -1091,189 +946,159 @@ def test_place_order_manage_server_overloaded_error_failure(self, mock_api): price=Decimal("2"), )) - def test_format_trading_rules__min_notional_present(self): - trading_rules = [{ - "symbol": "COINALPHAHBOT", - "baseSizePrecision": 1e-8, - "quotePrecision": 8, - "baseAssetPrecision": 8, - "status": "ENABLED", - "quoteAmountPrecision": "0.001", - "orderTypes": ["LIMIT", "MARKET"], - "filters": [ - { - "filterType": "PRICE_FILTER", - "minPrice": "0.00000100", - "maxPrice": "100000.00000000", - "tickSize": "0.00000100" - }, { - "filterType": "LOT_SIZE", - "minQty": "0.00100000", - "maxQty": "100000.00000000", - "stepSize": "0.00100000" - }, { - "filterType": "MIN_NOTIONAL", - "minNotional": "0.00300000" - } - ], - "permissions": [ - "SPOT" - ] - }] - exchange_info = {"symbols": trading_rules} - - result = self.async_run_with_timeout(self.exchange._format_trading_rules(exchange_info)) - - self.assertEqual(result[0].min_notional_size, Decimal("0.00100000")) - def _validate_auth_credentials_taking_parameters_from_argument(self, request_call_tuple: RequestCall, params: Dict[str, Any]): - self.assertIn("timestamp", params) - self.assertIn("signature", params) + self.assertIn("nonce", params) request_headers = request_call_tuple.kwargs["headers"] - self.assertIn("X-MEXC-APIKEY", request_headers) - self.assertEqual("testAPIKey", request_headers["X-MEXC-APIKEY"]) + self.assertIn("API-Sign", request_headers) + self.assertIn("API-Key", request_headers) + self.assertEqual("testAPIKey", request_headers["API-Key"]) def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "origClientOrderId": order.exchange_order_id or "dummyOrdId", - "orderId": 4, - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": str(order.price), - "origQty": str(order.amount), - "executedQty": str(Decimal("0")), - "cummulativeQuoteQty": str(Decimal("0")), - "status": "NEW", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "BUY" + "error": [], + "result": { + "count": 1 + } } def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: - return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": order.exchange_order_id, - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": str(order.price), - "origQty": str(order.amount), - "executedQty": str(order.amount), - "cummulativeQuoteQty": str(order.price + Decimal(2)), - "status": "FILLED", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "BUY", - "stopPrice": "0.0", - "icebergQty": "0.0", - "time": 1499827319559, - "updateTime": 1499827319559, - "isWorking": True, - "origQuoteOrderQty": str(order.price * order.amount) - } + return \ + { + "error": [], + "result": { + "open": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "closed", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } + } + } + } def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": order.exchange_order_id, - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": str(order.price), - "origQty": str(order.amount), - "executedQty": "0.0", - "cummulativeQuoteQty": "10000.0", - "status": "CANCELED", - "timeInForce": "GTC", - "type": order.order_type.name.upper(), - "side": order.trade_type.name.upper(), - "stopPrice": "0.0", - "icebergQty": "0.0", - "time": 1499827319559, - "updateTime": 1499827319559, - "isWorking": True, - "origQuoteOrderQty": str(order.price * order.amount) + "error": [], + "result": { + "count": 1 + } } def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": order.exchange_order_id, - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": str(order.price), - "origQty": str(order.amount), - "executedQty": "0.0", - "cummulativeQuoteQty": "10000.0", - "status": "NEW", - "timeInForce": "GTC", - "type": order.order_type.name.upper(), - "side": order.trade_type.name.upper(), - "stopPrice": "0.0", - "icebergQty": "0.0", - "time": 1499827319559, - "updateTime": 1499827319559, - "isWorking": True, - "origQuoteOrderQty": str(order.price * order.amount) + "error": [], + "result": { + "open": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } + } + } } def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: return { - "symbol": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "orderId": order.exchange_order_id, - "orderListId": -1, - "clientOrderId": order.client_order_id, - "price": str(order.price), - "origQty": str(order.amount), - "executedQty": str(order.amount), - "cummulativeQuoteQty": str(self.expected_partial_fill_amount * order.price), - "status": "PARTIALLY_FILLED", - "timeInForce": "GTC", - "type": order.order_type.name.upper(), - "side": order.trade_type.name.upper(), - "stopPrice": "0.0", - "icebergQty": "0.0", - "time": 1499827319559, - "updateTime": 1499827319559, - "isWorking": True, - "origQuoteOrderQty": str(order.price * order.amount) + "error": [], + "result": { + "open": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount / 2), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } + } + } } def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): - return [ - { - "symbol": self.exchange_symbol_for_tokens(order.base_asset, order.quote_asset), - "id": self.expected_fill_trade_id, - "orderId": int(order.exchange_order_id), - "orderListId": -1, - "price": str(self.expected_partial_fill_price), - "qty": str(self.expected_partial_fill_amount), - "quoteQty": str(self.expected_partial_fill_amount * self.expected_partial_fill_price), - "commission": str(self.expected_fill_fee.flat_fees[0].amount), - "commissionAsset": self.expected_fill_fee.flat_fees[0].token, - "time": 1499865549590, - "isBuyer": True, - "isMaker": False, - "isBestMatch": True + return { + "error": [], + "result": { + self.expected_fill_trade_id: { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(self.expected_partial_fill_price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(self.expected_partial_fill_amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" + } } - ] + } + def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): - return [ - { - "symbol": self.exchange_symbol_for_tokens(order.base_asset, order.quote_asset), - "id": self.expected_fill_trade_id, - "orderId": int(order.exchange_order_id), - "orderListId": -1, - "price": str(order.price), - "qty": str(order.amount), - "quoteQty": str(order.amount * order.price), - "commission": str(self.expected_fill_fee.flat_fees[0].amount), - "commissionAsset": self.expected_fill_fee.flat_fees[0].token, - "time": 1499865549590, - "isBuyer": True, - "isMaker": False, - "isBestMatch": True + return { + "error": [], + "result": { + self.expected_fill_trade_id: { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(order.price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(order.amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" + } + } } - ] From 7de82690bf87a6414921680a6e9c070576bb320c Mon Sep 17 00:00:00 2001 From: bczhang Date: Mon, 19 Feb 2024 06:11:11 +0800 Subject: [PATCH 12/34] update unittest --- .../kraken_api_user_stream_data_source.py | 2 +- .../exchange/kraken/kraken_exchange.py | 3 +- .../exchange/kraken/kraken_order_book.py | 8 +- .../test_kraken_api_order_book_data_source.py | 4 + ...test_kraken_api_user_stream_data_source.py | 14 +- .../exchange/kraken/test_kraken_auth.py | 19 +- .../exchange/kraken/test_kraken_exchange.py | 195 ++++++++++++------ .../kraken/test_kraken_in_flight_order.py | 10 +- .../exchange/kraken/test_kraken_order_book.py | 15 +- .../exchange/kraken/test_kraken_utils.py | 6 +- .../exchange/kraken/test_kraken_web_utils.py | 4 +- 11 files changed, 179 insertions(+), 101 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index cf7621021d..73c96a048f 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -19,10 +19,10 @@ def __init__(self, connector: 'KrakenExchange', api_factory: Optional[WebAssistantsFactory] = None): + super().__init__() self._api_factory = api_factory self._connector = connector self._current_auth_token: Optional[str] = None - super().__init__() async def _connected_websocket_assistant(self) -> WSAssistant: ws: WSAssistant = await self._api_factory.get_ws_assistant() diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 4c4ec45a9d..f16f7085e2 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -60,9 +60,10 @@ def __init__(self, self._trading_required = trading_required self._trading_pairs = trading_pairs self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) - self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) self._asset_pairs = {} self._last_userref = 0 + self._client_config = client_config_map + self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) super().__init__(client_config_map) diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.py b/hummingbot/connector/exchange/kraken/kraken_order_book.py index 328b6c257f..ac98bf43ea 100644 --- a/hummingbot/connector/exchange/kraken/kraken_order_book.py +++ b/hummingbot/connector/exchange/kraken/kraken_order_book.py @@ -25,7 +25,7 @@ def snapshot_message_from_exchange(cls, "update_id": msg["latest_update"], "bids": msg["bids"], "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) + }, timestamp=timestamp) @classmethod def diff_message_from_exchange(cls, @@ -39,7 +39,7 @@ def diff_message_from_exchange(cls, "update_id": msg["update_id"], "bids": msg["bids"], "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) + }, timestamp=timestamp) @classmethod def snapshot_ws_message_from_exchange(cls, @@ -53,7 +53,7 @@ def snapshot_ws_message_from_exchange(cls, "update_id": msg["update_id"], "bids": msg["bids"], "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) + }, timestamp=timestamp) @classmethod def trade_message_from_exchange(cls, msg: Dict[str, any], metadata: Optional[Dict] = None): @@ -67,7 +67,7 @@ def trade_message_from_exchange(cls, msg: Dict[str, any], metadata: Optional[Dic "update_id": ts, "price": msg["trade"][0], "amount": msg["trade"][1] - }, timestamp=ts * 1e-3) + }, timestamp=ts) @classmethod def from_snapshot(cls, msg: OrderBookMessage) -> "OrderBook": diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py index d59b63b9f6..fb78678ca8 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py @@ -21,6 +21,8 @@ class KrakenAPIOrderBookDataSourceTest(unittest.TestCase): + level = 0 + @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -51,6 +53,8 @@ def setUp(self) -> None: api_factory=self.connector._web_assistants_factory, trading_pairs=[self.trading_pair]) + self._original_full_order_book_reset_time = self.data_source.FULL_ORDER_BOOK_RESET_DELTA_SECONDS + self.data_source.FULL_ORDER_BOOK_RESET_DELTA_SECONDS = -1 self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index ffc063e4ab..abe45e8a6f 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -21,6 +21,8 @@ class KrakenAPIUserStreamDataSourceTest(unittest.TestCase): + level = 0 + @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -46,7 +48,7 @@ def setUp(self) -> None: client_config_map=client_config_map, kraken_api_key="", kraken_secret_key="", - trading_pairs=[], + trading_pairs=[self.trading_pair], trading_required=False) not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" @@ -63,7 +65,16 @@ def setUp(self) -> None: self.resume_test_event = asyncio.Event() self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair})) + def tearDown(self) -> None: + self.listening_task and self.listening_task.cancel() + super().tearDown() + + def handle(self, record): + self.log_records.append(record) + def _is_logged(self, log_level: str, message: str) -> bool: + return any(record.levelname == log_level and record.getMessage() == message + for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret @@ -146,7 +157,6 @@ def test_listen_for_user_stream(self, mocked_api, ws_connect_mock): regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_auth_response_mock() mocked_api.post(regex_url, body=json.dumps(resp)) - ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() output_queue = asyncio.Queue() self.ev_loop.create_task(self.data_source.listen_for_user_stream(output_queue)) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py index cdad78296a..37e4b55f19 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py @@ -4,7 +4,7 @@ import hmac from copy import copy from unittest import TestCase -from unittest.mock import MagicMock +from unittest.mock import MagicMock,patch from typing_extensions import Awaitable @@ -16,13 +16,15 @@ class KrakenAuthTests(TestCase): def setUp(self) -> None: self._api_key = "testApiKey" - self._secret = "testSecret" + self._secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" # noqa: mock def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret - def test_rest_authenticate(self): + @patch("hummingbot.connector.exchange.kraken.kraken_auth.KrakenAuth.get_tracking_nonce") + def test_rest_authenticate(self, mocked_nonce): + mocked_nonce.return_value = "1" now = 1234567890.000 mock_time_provider = MagicMock() mock_time_provider.time.return_value = now @@ -35,24 +37,23 @@ def test_rest_authenticate(self): "quantity": 1, "price": "0.1", } - full_params = copy(params) auth = KrakenAuth(api_key=self._api_key, secret_key=self._secret, time_provider=mock_time_provider) - request = RESTRequest(method=RESTMethod.GET, params=params, is_auth_required=True) + request = RESTRequest(method=RESTMethod.GET, data=params, is_auth_required=True) request.url = test_url configured_request = self.async_run_with_timeout(auth.rest_authenticate(request)) # full_params.update({"timestamp": 1234567890000}) - + api_secret = base64.b64decode(self._secret) api_path: bytes = bytes(request.url, 'utf-8') - api_nonce: str = "1234567890000" + api_nonce: str = "1" api_post: str = "nonce=" + api_nonce for key, value in params.items(): api_post += f"&{key}={value}" api_sha256: bytes = hashlib.sha256(bytes(api_nonce + api_post, 'utf-8')).digest() - api_hmac: hmac.HMAC = hmac.new(self._secret.encode("utf-8"), api_path + api_sha256, hashlib.sha512) + api_hmac: hmac.HMAC = hmac.new(api_secret, api_path + api_sha256, hashlib.sha512) expected_signature: bytes = base64.b64encode(api_hmac.digest()) # # expected_signature = hmac.new( @@ -60,5 +61,5 @@ def test_rest_authenticate(self): # encoded_params.encode("utf-8"), # hashlib.sha256).hexdigest() # self.assertEqual(now * 1e3, configured_request.params["timestamp"]) - self.assertEqual(str(expected_signature, 'utf-8'), configured_request.headers["signature"]["API-Sign"]) + self.assertEqual(str(expected_signature, 'utf-8'), configured_request.headers["API-Sign"]) self.assertEqual(self._api_key, configured_request.headers["API-Key"]) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index f6898e08bf..b08135696d 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -1,5 +1,5 @@ -import asyncio import json +import logging import re from decimal import Decimal from typing import Any, Callable, Dict, List, Optional, Tuple @@ -25,6 +25,18 @@ class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): + _logger = logging.getLogger(__name__) + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.api_key = "someKey" + cls.api_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" # noqa: mock + cls.base_asset = "BTC" + cls.quote_asset = "USDT" # linear + cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" + cls.ex_trading_pair = cls.base_asset + cls.quote_asset + cls.ws_ex_trading_pairs = cls.base_asset + "/" + cls.quote_asset @property def all_symbols_url(self): @@ -56,10 +68,59 @@ def balance_url(self): url = web_utils.private_rest_url(CONSTANTS.BALANCE_PATH_URL) return url + @property + def latest_prices_request_mock_response(self): + return { + "error": [], + "result": { + "XXBTZUSD": { + "a": [ + "30300.10000", + "1", + "1.000" + ], + "b": [ + "30300.00000", + "1", + "1.000" + ], + "c": [ + "30303.20000", + "0.00067643" + ], + "v": [ + "4083.67001100", + "4412.73601799" + ], + "p": [ + "30706.77771", + "30689.13205" + ], + "t": [ + 34619, + 38907 + ], + "l": [ + "29868.30000", + "29868.30000" + ], + "h": [ + "31631.00000", + "31631.00000" + ], + "o": "30502.80000" + } + } + } + + @property + def balance_event_websocket_update(self): + pass + @property def all_symbols_request_mock_response(self): return { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), "wsname": f"{self.base_asset}/{self.quote_asset}", "aclass_base": "currency", @@ -104,7 +165,7 @@ def all_symbols_request_mock_response(self): @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: response = { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), "wsname": f"{self.base_asset}/{self.quote_asset}", "aclass_base": "currency", @@ -281,8 +342,6 @@ def trading_rules_request_erroneous_mock_response(self): @property def order_creation_request_successful_mock_response(self): return { - "error": [], - "result": { "descr": { "order": "", }, @@ -290,7 +349,6 @@ def order_creation_request_successful_mock_response(self): self.expected_exchange_order_id, ] } - } @property def balance_request_mock_response_for_base_and_quote(self): @@ -374,8 +432,8 @@ def create_exchange_instance(self): client_config_map = ClientConfigAdapter(ClientConfigMap()) return KrakenExchange( client_config_map=client_config_map, - kraken_api_key="testAPIKey", - kraken_secret_key="testSecret", + kraken_api_key=self.api_key, + kraken_secret_key=self.api_secret, trading_pairs=[self.trading_pair], ) @@ -413,7 +471,7 @@ def configure_successful_cancelation_response( url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_cancelation_request_successful_mock_response(order=order) - mock_api.delete(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_erroneous_cancelation_response( @@ -423,7 +481,7 @@ def configure_erroneous_cancelation_response( callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - mock_api.delete(regex_url, status=400, callback=callback) + mock_api.post(regex_url, status=400, callback=callback) return url def configure_order_not_found_error_cancelation_response( @@ -437,7 +495,7 @@ def configure_order_not_found_error_cancelation_response( "API key doesn't have permission to make this request" ] } - mock_api.delete(regex_url, status=400, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, status=400, body=json.dumps(response), callback=callback) return url def configure_one_successful_one_erroneous_cancel_all_response( @@ -471,7 +529,7 @@ def configure_canceled_order_status_response( order: InFlightOrder, mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: - url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) + url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_canceled_mock_response(order=order) mock_api.get(regex_url, body=json.dumps(response), callback=callback) @@ -556,42 +614,42 @@ def configure_full_fill_trade_response( def order_event_for_new_order_websocket_update(self, order: InFlightOrder): return [ - [ - { - order.exchange_order_id: { - "avg_price": "34.50000", - "cost": "0.00000", - "descr": { - "close": "", - "leverage": "0:1", - "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", - "ordertype": "limit", - "pair": convert_to_exchange_trading_pair(self.trading_pair, '/'), - "price": str(order.price), - "price2": "0.00000", - "type": "sell" - }, - "expiretm": "0.000000", - "fee": "0.00000", - "limitprice": "34.50000", - "misc": "", - "oflags": "fcib", - "opentm": "0.000000", - "refid": "OKIVMP-5GVZN-Z2D2UA", - "starttm": "0.000000", - "status": "open", - "stopprice": "0.000000", - "userref": 0, - "vol": str(order.amount,), - "vol_exec": "0.00000000" - } - } - ], - "openOrders", + [ { - "sequence": 234 + order.exchange_order_id: { + "avg_price": "34.50000", + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", + "ordertype": "limit", + "pair": convert_to_exchange_trading_pair(self.trading_pair, '/'), + "price": str(order.price), + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "34.50000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "open", + "stopprice": "0.000000", + "userref": 0, + "vol": str(order.amount, ), + "vol_exec": "0.00000000" + } } - ] + ], + "openOrders", + { + "sequence": 234 + } + ] def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): return { @@ -667,6 +725,9 @@ def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): def test_update_time_synchronizer_successfully(self, mock_api, seconds_counter_mock): pass + def test_user_stream_balance_update(self): + pass + @aioresponses() def test_update_time_synchronizer_failure_is_logged(self, mock_api): pass @@ -689,6 +750,8 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), + userref=1, + ) order = self.exchange.in_flight_orders["OID1"] @@ -781,7 +844,6 @@ def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" )) - @aioresponses() def test_update_order_status_when_failed(self, mock_api): self.exchange._set_current_timestamp(1640780000) @@ -796,6 +858,7 @@ def test_update_order_status_when_failed(self, mock_api): trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), + userref=1, ) order = self.exchange.in_flight_orders["OID1"] @@ -829,7 +892,6 @@ def test_update_order_status_when_failed(self, mock_api): } } - mock_response = order_status mock_api.get(regex_url, body=json.dumps(mock_response)) @@ -1079,26 +1141,25 @@ def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): } } - def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): return { - "error": [], - "result": { - self.expected_fill_trade_id: { - "ordertxid": "OQCLML-BW3P3-BUCMWZ", - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(order.price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(order.amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" - } + "error": [], + "result": { + self.expected_fill_trade_id: { + "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(order.price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(order.amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" } } + } diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py index ed921aec7c..e30b15c4b2 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py @@ -46,9 +46,9 @@ def test_serialize_order_to_json(self): "amount": str(order.amount), "executed_amount_base": str(order.executed_amount_base), "executed_amount_quote": str(order.executed_amount_quote), - "last_state": OrderState.OPEN, - "leverage": 1, - "position": PositionAction.NIL, + "last_state": str(OrderState.OPEN.value), + "leverage": "1", + "position": PositionAction.NIL.value, "userref": 2, "creation_timestamp": 1640001112.0, "last_update_timestamp": 1640001112.0, @@ -66,7 +66,9 @@ def test_deserialize_order_from_json(self): "trade_type": TradeType.BUY.name, "price": "1000", "amount": "1", - "last_state": "OPEN", + "last_state": "1", + "leverage": "1", + "position": PositionAction.NIL.value, "executed_amount_base": "0.1", "executed_amount_quote": "110", "fee_asset": "BNB", diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py index 3f2a1c8719..c41042a205 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py @@ -9,7 +9,7 @@ class KrakenOrderBookTests(TestCase): def test_snapshot_message_from_exchange(self): snapshot_message = KrakenOrderBook.snapshot_message_from_exchange( msg={ - "lastUpdateId": 1, + "latest_update": 1, "bids": [ ["4.00000000", "431.00000000"] ], @@ -18,7 +18,7 @@ def test_snapshot_message_from_exchange(self): ] }, timestamp=1640000000.0, - metadata={"trading_pair": "COINALPHA/HBOT"} + metadata={"trading_pair": "COINALPHA-HBOT"} ) self.assertEqual("COINALPHA-HBOT", snapshot_message.trading_pair) @@ -52,9 +52,10 @@ def test_diff_message_from_exchange(self): "1.52900000", "1534614248.765567" ], - ] + ], + "update_id":3407459756 }, - timestamp=1640000000000, + timestamp=1640000000, ) self.assertEqual("COINALPHA-HBOT", diff_msg.trading_pair) @@ -65,11 +66,9 @@ def test_diff_message_from_exchange(self): self.assertEqual(1, len(diff_msg.bids)) self.assertEqual(5541.2, diff_msg.bids[0].price) self.assertEqual(1.529, diff_msg.bids[0].amount) - self.assertEqual(1534614248.765567, diff_msg.bids[0].update_id) self.assertEqual(1, len(diff_msg.asks)) self.assertEqual(5541.3, diff_msg.asks[0].price) self.assertEqual(2.507, diff_msg.asks[0].amount) - self.assertEqual(1534614248.765567, diff_msg.asks[0].update_id) def test_trade_message_from_exchange(self): trade_update = { @@ -92,5 +91,5 @@ def test_trade_message_from_exchange(self): self.assertEqual("COINALPHA-HBOT", trade_message.trading_pair) self.assertEqual(OrderBookMessageType.TRADE, trade_message.type) self.assertEqual(1534614057.321597, trade_message.timestamp) - self.assertEqual(1534614057.321597, trade_message.update_id) - self.assertEqual(1534614057321.597, trade_message.trade_id) + self.assertEqual(-1, trade_message.update_id) + self.assertEqual(1534614057.321597, trade_message.trade_id) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py index 10851be408..fec2f7b3a7 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py @@ -26,15 +26,15 @@ def test_convert_to_exchange_symbol(self): def test_convert_to_exchange_trading_pair(self): self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.hb_trading_pair)) - self.assertEqual(self.ex_ws_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_ws_trading_pair)) + self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_ws_trading_pair)) self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_trading_pair)) def test_split_to_base_quote(self): self.assertEqual((self.hb_base_asset,self.quote_asset), utils.split_to_base_quote(self.trading_pair)) def test_convert_from_exchange_trading_pair(self): - self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair)) - self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair,("XBTUSDT","ETHUSDT"))) + self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.trading_pair)) + self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair,("BTC-USDT","ETH-USDT"))) self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_ws_trading_pair)) def test_build_rate_limits_by_tier(self): diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py b/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py index a8d120fe49..ce0af60fd7 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_web_utils.py @@ -28,7 +28,7 @@ def test_is_exchange_information_valid(self): } } - self.assertFalse(web_utils.is_exchange_information_valid(invalid_info_1)) + self.assertFalse(web_utils.is_exchange_information_valid(invalid_info_1["XBTUSDT"])) valid_info_1 = { "XBTUSDT": { "altname": "XBTUSDT", @@ -40,4 +40,4 @@ def test_is_exchange_information_valid(self): } } - self.assertTrue(web_utils.is_exchange_information_valid(valid_info_1)) + self.assertTrue(web_utils.is_exchange_information_valid(valid_info_1["XBTUSDT"])) From f0f51140b112dd190ad64ad39ef62f8de28d3cf1 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 04:42:41 +0800 Subject: [PATCH 13/34] update unittest --- .../connector/exchange/kraken/kraken_auth.py | 8 +- .../exchange/kraken/kraken_constants.py | 2 +- .../exchange/kraken/kraken_exchange.py | 65 +- .../exchange/kraken/kraken_in_fight_order.py | 15 + .../connector/exchange/kraken/kraken_utils.py | 7 + .../exchange/kraken/test_kraken_auth.py | 3 +- .../exchange/kraken/test_kraken_exchange.py | 1207 +++++++++-------- 7 files changed, 710 insertions(+), 597 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index 5b68fcb194..bc31fc8de0 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -7,6 +7,7 @@ import hashlib import hmac import time +import json from hummingbot.connector.time_synchronizer import TimeSynchronizer from hummingbot.core.web_assistant.auth import AuthBase @@ -31,10 +32,13 @@ async def rest_authenticate(self, request: RESTRequest) -> RESTRequest: headers = {} if request.headers is not None: headers.update(request.headers) - auth_dict: Dict[str, Any] = self._generate_auth_dict(request.url, request.data) + + data = json.loads(request.data) if request.data is not None else {} + + auth_dict: Dict[str, Any] = self._generate_auth_dict(request.url, data) headers.update(auth_dict["headers"]) request.headers = headers - request.data = auth_dict["postDict"] + request.data = json.dumps(auth_dict["postDict"]) return request async def ws_authenticate(self, request: WSRequest) -> WSRequest: diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index 1fae427aea..4d05b54b93 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -59,7 +59,7 @@ class KrakenAPITier(Enum): ORDER_STATE = { "pending": OrderState.PENDING_CREATE, "open": OrderState.OPEN, - "closed": OrderState.COMPLETED, + "closed": OrderState.FILLED, "canceled": OrderState.CANCELED, "expired": OrderState.FAILED, } diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index f16f7085e2..fb5fdccf37 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -31,6 +31,7 @@ from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.core.utils.estimate_fee import build_trade_fee from hummingbot.core.web_assistant.connections.data_types import RESTMethod from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -179,7 +180,17 @@ def _get_fee(self, price: Decimal = s_decimal_NaN, is_maker: Optional[bool] = None) -> TradeFeeBase: is_maker = order_type is OrderType.LIMIT_MAKER - return DeductedFromReturnsTradeFee(percent=self.estimate_fee_pct(is_maker)) + trade_base_fee = build_trade_fee( + exchange=self.name, + is_maker=is_maker, + order_side=order_side, + order_type=order_type, + amount=amount, + price=price, + base_currency=base_currency, + quote_currency=quote_currency + ) + return trade_base_fee def generate_userref(self): self._last_userref += 1 @@ -270,7 +281,7 @@ async def get_asset_pairs(self) -> Dict[str, Any]: if not self._asset_pairs: asset_pairs = await self._api_request(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) self._asset_pairs = {f"{details['base']}-{details['quote']}": details - for _, details in asset_pairs.items() if + for _, details in asset_pairs["result"].items() if web_utils.is_exchange_information_valid(details)} return self._asset_pairs @@ -317,7 +328,7 @@ def restore_tracking_states(self, saved_states: Dict[str, Any]): elif order.is_failure: # If the order is marked as failed but is still in the tracking states, it was a lost order self._order_tracker._lost_orders[order.client_order_id] = order - self._last_userref = max(int(serialized_order.userref), self._last_userref) + self._last_userref = max(int(order.userref), self._last_userref) async def _place_order(self, order_id: str, @@ -371,7 +382,7 @@ async def _api_request_with_retry(self, if endpoint == CONSTANTS.ADD_ORDER_PATH_URL: self.logger().info(f"Retrying {endpoint}") # Order placement could have been successful despite the IOError, so check for the open order. - response = self.get_open_orders_with_userref(data.get('userref')) + response = await self.get_open_orders_with_userref(data.get('userref')) if any(response.get("open").values()): return response self.logger().warning( @@ -396,7 +407,7 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): data=api_params, is_auth_required=True) if isinstance(cancel_result, dict) and ( - cancel_result.get("count") == 1 or cancel_result.get("error") is not None): + cancel_result.get("result",{}).get("count") == 1 or cancel_result.get("result",{}).get("error") is not None): return True return False @@ -447,11 +458,11 @@ async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> Lis } """ retval: list = [] - trading_pair_rules = exchange_info_dict.values() + trading_pair_rules = exchange_info_dict["result"].values() # for trading_pair, rule in asset_pairs_dict.items(): for rule in filter(web_utils.is_exchange_information_valid, trading_pair_rules): try: - trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol=rule.get("symbol")) + trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol=rule.get("altname")) min_order_size = Decimal(rule.get('ordermin', 0)) min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") @@ -497,11 +508,6 @@ async def _user_stream_event_listener(self): "Unexpected error in user stream listener loop.", exc_info=True) await self._sleep(5.0) - def _process_balance_message_ws(self, account): - asset_name = account["a"] - self._account_available_balances[asset_name] = Decimal(str(account["f"])) - self._account_balances[asset_name] = Decimal(str(account["f"])) + Decimal(str(account["l"])) - def _create_trade_update_with_order_fill_data( self, order_fill: Dict[str, Any], @@ -537,7 +543,7 @@ def _process_trade_message(self, trades: List): trade["trade_id"] = trade_id exchange_order_id = trade.get("ordertxid") try: - client_order_id = next(key for key, value in self._in_flight_orders.items() + client_order_id = next(key for key, value in self.in_flight_orders.items() if value.exchange_order_id == exchange_order_id) except StopIteration: continue @@ -562,16 +568,16 @@ def _create_order_update_with_order_status_data(self, order_status: Dict[str, An return order_update def _process_order_message(self, orders: List): - for update in orders: - for exchange_order_id, order_msg in update.items(): - tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) - if not tracked_order: - self.logger().debug( - f"Ignoring order message with id {tracked_order.client_order_id}: not in in_flight_orders.") - return - order_update = self._create_order_update_with_order_status_data(order_status=order_msg, - order=tracked_order) - self._order_tracker.process_order_update(order_update=order_update) + update = orders[0] + for exchange_order_id, order_msg in update[0].items(): + tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) + if not tracked_order: + self.logger().debug( + f"Ignoring order message with id {tracked_order.client_order_id}: not in in_flight_orders.") + return + order_update = self._create_order_update_with_order_status_data(order_status=order_msg, + order=tracked_order) + self._order_tracker.process_order_update(order_update=order_update) async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: trade_updates = [] @@ -584,7 +590,7 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade data={"txid": exchange_order_id}, is_auth_required=True) - for trade_id, trade_fill in all_fills_response.items(): + for trade_id, trade_fill in all_fills_response["result"].items(): trade: Dict[str, str] = all_fills_response[trade_id] trade["trade_id"] = trade_id trade_update = self._create_trade_update_with_order_fill_data( @@ -605,7 +611,7 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda params={"txid": tracked_order.exchange_order_id}, is_auth_required=True) - update = updated_order_data.get(tracked_order.exchange_order_id) + update = updated_order_data["result"].get(tracked_order.exchange_order_id) if update.get("error") is not None and "EOrder:Invalid order" not in update["error"]: self.logger().debug(f"Error in fetched status update for order {tracked_order.client_order_id}: " @@ -617,7 +623,7 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda client_order_id=tracked_order.client_order_id, exchange_order_id=tracked_order.exchange_order_id, trading_pair=tracked_order.trading_pair, - update_timestamp=self._current_timestamp, + update_timestamp=self.current_timestamp, new_state=new_state, ) @@ -633,7 +639,7 @@ async def _update_balances(self): locked = defaultdict(Decimal) - for order in open_orders.get("open").values(): + for order in open_orders["result"].get("open").values(): if order.get("status") == "open": details = order.get("descr") if details.get("ordertype") == "limit": @@ -647,7 +653,7 @@ async def _update_balances(self): elif details.get("type") == "buy": locked[convert_from_exchange_symbol(quote)] += vol_locked * Decimal(details.get("price")) - for asset_name, balance in balances.items(): + for asset_name, balance in balances["result"].items(): cleaned_name = convert_from_exchange_symbol(asset_name).upper() total_balance = Decimal(balance) free_balance = total_balance - Decimal(locked[cleaned_name]) @@ -662,7 +668,7 @@ async def _update_balances(self): def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]): mapping = bidict() - for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info.values()): + for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info["result"].values()): mapping[symbol_data["altname"]] = combine_to_hb_trading_pair(base=symbol_data["base"], quote=symbol_data["quote"]) self._set_trading_pair_symbol_map(mapping) @@ -671,7 +677,6 @@ async def _get_last_traded_price(self, trading_pair: str) -> float: params = { "pair": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) } - resp_json = await self._api_request( method=RESTMethod.GET, path_url=CONSTANTS.TICKER_PATH_URL, diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index de7cf56722..2533a53f81 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -4,6 +4,7 @@ from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate +import math class KrakenInFlightOrder(InFlightOrder): @@ -38,6 +39,15 @@ def __init__( ) self.userref = userref + @property + def is_done(self) -> bool: + return ( + self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} + or math.isclose(self.executed_amount_base, self.amount) + or self.executed_amount_base >= self.amount + # or self. + ) + @property def attributes(self) -> Tuple[Any]: return copy.deepcopy( @@ -116,3 +126,8 @@ def to_json(self) -> Dict[str, Any]: "last_update_timestamp": self.last_update_timestamp, "order_fills": {key: fill.to_json() for key, fill in self.order_fills.items()} } + + def check_filled_condition(self): + if (abs(self.amount) - self.executed_amount_base).quantize(Decimal('1e-8')) <= 0 \ + or self.current_state == OrderState.FILLED: + self.completely_filled_event.set() diff --git a/hummingbot/connector/exchange/kraken/kraken_utils.py b/hummingbot/connector/exchange/kraken/kraken_utils.py index 7573e350fe..266bdb5c6c 100644 --- a/hummingbot/connector/exchange/kraken/kraken_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_utils.py @@ -127,6 +127,13 @@ def _build_private_rate_limits(tier: KrakenAPITier = KrakenAPITier.STARTER) -> L weight=2, linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], ), + RateLimit( + limit_id=CONSTANTS.QUERY_TRADES_PATH_URL, + limit=PRIVATE_ENDPOINT_LIMIT, + time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, + weight=2, + linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], + ), ]) # Matching Engine Limits diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py index 37e4b55f19..70452456e6 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py @@ -2,6 +2,7 @@ import base64 import hashlib import hmac +import json from copy import copy from unittest import TestCase from unittest.mock import MagicMock,patch @@ -39,7 +40,7 @@ def test_rest_authenticate(self, mocked_nonce): } auth = KrakenAuth(api_key=self._api_key, secret_key=self._secret, time_provider=mock_time_provider) - request = RESTRequest(method=RESTMethod.GET, data=params, is_auth_required=True) + request = RESTRequest(method=RESTMethod.GET, data=json.dumps(params), is_auth_required=True) request.url = test_url configured_request = self.async_run_with_timeout(auth.rest_authenticate(request)) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index b08135696d..0949286523 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -12,16 +12,14 @@ from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange -from hummingbot.connector.exchange.kraken.kraken_utils import ( - convert_to_exchange_trading_pair, -) +from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder from hummingbot.connector.test_support.exchange_connector_test import AbstractExchangeConnectorTests from hummingbot.connector.trading_rule import TradingRule from hummingbot.connector.utils import get_new_client_order_id from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState -from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase -from hummingbot.core.event.events import MarketOrderFailureEvent, OrderFilledEvent +from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TokenAmount, TradeFeeBase +from hummingbot.core.event.events import MarketOrderFailureEvent class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): @@ -32,11 +30,13 @@ def setUpClass(cls) -> None: super().setUpClass() cls.api_key = "someKey" cls.api_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" # noqa: mock - cls.base_asset = "BTC" + cls.base_asset = "ETH" + cls.ex_base_asset = "ETH" cls.quote_asset = "USDT" # linear cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" - cls.ex_trading_pair = cls.base_asset + cls.quote_asset - cls.ws_ex_trading_pairs = cls.base_asset + "/" + cls.quote_asset + cls.ex_trading_pair = cls.ex_base_asset + cls.quote_asset + cls.ws_ex_trading_pairs = cls.ex_base_asset + "/" + cls.quote_asset + cls._userref = 0 @property def all_symbols_url(self): @@ -45,7 +45,7 @@ def all_symbols_url(self): @property def latest_prices_url(self): url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_PATH_URL) - url = f"{url}?symbol={self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset)}" + url = f"{url}?pair={self.ex_trading_pair}" return url @property @@ -73,7 +73,7 @@ def latest_prices_request_mock_response(self): return { "error": [], "result": { - "XXBTZUSD": { + self.ex_trading_pair: { "a": [ "30300.10000", "1", @@ -85,7 +85,7 @@ def latest_prices_request_mock_response(self): "1.000" ], "c": [ - "30303.20000", + self.expected_latest_price, "0.00067643" ], "v": [ @@ -120,133 +120,139 @@ def balance_event_websocket_update(self): @property def all_symbols_request_mock_response(self): return { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + "error": [], + "result": + { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } + } } @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: response = { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - }, - "ETHUSDT.d": { - "altname": "ETHUSDT.d", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" + "error": [], + "result": { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + }, + "ETHUSDT.d": { + "altname": "ETHUSDT.d", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } } - return "INVALID-PAIR", response @property @@ -256,99 +262,105 @@ def network_status_request_successful_mock_response(self): @property def trading_rules_request_mock_response(self): return { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" + "error": [], + "result": { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } } @property def trading_rules_request_erroneous_mock_response(self): return { - "XBTUSDT": { - "altname": "XBTUSDT", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, + "error": [], + "result": { + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + } } } @property def order_creation_request_successful_mock_response(self): return { - "descr": { - "order": "", - }, - "txid": [ - self.expected_exchange_order_id, - ] - } + "descr": { + "order": "", + }, + "txid": [ + self.expected_exchange_order_id, + ] + } @property def balance_request_mock_response_for_base_and_quote(self): @@ -379,7 +391,7 @@ def expected_supported_order_types(self): @property def expected_trading_rule(self): - rule = list(self.trading_rules_request_mock_response.values())[0] + rule = list(self.trading_rules_request_mock_response["result"].values())[0] min_order_size = Decimal(rule.get('ordermin', 0)) min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") @@ -392,7 +404,7 @@ def expected_trading_rule(self): @property def expected_logged_error_for_erroneous_trading_rule(self): - erroneous_rule = self.trading_rules_request_erroneous_mock_response["symbols"][0] + erroneous_rule = list(self.trading_rules_request_erroneous_mock_response["result"].values())[0] return f"Error parsing the trading pair rule {erroneous_rule}. Skipping." @property @@ -401,7 +413,7 @@ def expected_exchange_order_id(self): @property def is_order_fill_http_update_included_in_status_update(self) -> bool: - return True + return False @property def is_order_fill_http_update_executed_during_websocket_order_event_processing(self) -> bool: @@ -417,7 +429,7 @@ def expected_partial_fill_amount(self) -> Decimal: @property def expected_fill_fee(self) -> TradeFeeBase: - return DeductedFromReturnsTradeFee( + return AddedToCostTradeFee( percent_token=self.quote_asset, flat_fees=[TokenAmount(token=self.quote_asset, amount=Decimal("30"))]) @@ -440,19 +452,19 @@ def create_exchange_instance(self): def validate_auth_credentials_present(self, request_call: RequestCall): self._validate_auth_credentials_taking_parameters_from_argument( request_call_tuple=request_call, - params=request_call.kwargs["data"] + params=json.loads(request_call.kwargs["data"]) ) def validate_order_creation_request(self, order: InFlightOrder, request_call: RequestCall): - request_data = dict(request_call.kwargs["data"]) + request_data = json.loads(request_call.kwargs["data"]) self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_data["pair"]) - self.assertEqual(order.trade_type.name.upper(), request_data["type"]) + self.assertEqual(order.trade_type.name.lower(), request_data["type"]) self.assertEqual(KrakenExchange.kraken_order_type(OrderType.LIMIT), request_data["ordertype"]) self.assertEqual(Decimal("100"), Decimal(request_data["volume"])) self.assertEqual(Decimal("10000"), Decimal(request_data["price"])) def validate_order_cancelation_request(self, order: InFlightOrder, request_call: RequestCall): - request_data = dict(request_call.kwargs["data"]) + request_data = json.loads(request_call.kwargs["data"]) self.assertEqual(order.exchange_order_id, request_data["txid"]) def validate_order_status_request(self, order: InFlightOrder, request_call: RequestCall): @@ -460,9 +472,16 @@ def validate_order_status_request(self, order: InFlightOrder, request_call: Requ self.assertEqual(order.exchange_order_id, request_params["txid"]) def validate_trades_request(self, order: InFlightOrder, request_call: RequestCall): - request_params = request_call.kwargs["data"] + request_params = json.loads(request_call.kwargs["data"]) self.assertEqual(order.exchange_order_id, str(request_params["txid"])) + def configure_order_not_found_error_cancelation_response( + self, order: InFlightOrder, mock_api: aioresponses, + callback: Optional[Callable] = lambda *args, **kwargs: None + ) -> str: + # Implement the expected not found response when enabling test_cancel_order_not_found_in_the_exchange + raise NotImplementedError + def configure_successful_cancelation_response( self, order: InFlightOrder, @@ -484,20 +503,6 @@ def configure_erroneous_cancelation_response( mock_api.post(regex_url, status=400, callback=callback) return url - def configure_order_not_found_error_cancelation_response( - self, order: InFlightOrder, mock_api: aioresponses, - callback: Optional[Callable] = lambda *args, **kwargs: None - ) -> str: - url = web_utils.private_rest_url(CONSTANTS.CANCEL_ORDER_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - response = { - "error": [ - "API key doesn't have permission to make this request" - ] - } - mock_api.post(regex_url, status=400, body=json.dumps(response), callback=callback) - return url - def configure_one_successful_one_erroneous_cancel_all_response( self, successful_order: InFlightOrder, @@ -519,9 +524,9 @@ def configure_completely_filled_order_status_response( mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?") + ".*") response = self._order_status_request_completely_filled_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_canceled_order_status_response( @@ -530,9 +535,9 @@ def configure_canceled_order_status_response( mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?") + ".*") response = self._order_status_request_canceled_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_erroneous_http_fill_trade_response( @@ -542,7 +547,7 @@ def configure_erroneous_http_fill_trade_response( callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") - mock_api.get(regex_url, status=400, callback=callback) + mock_api.post(regex_url, status=400, callback=callback) return url def configure_open_order_status_response( @@ -556,7 +561,7 @@ def configure_open_order_status_response( url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_open_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_http_error_order_status_response( @@ -566,7 +571,7 @@ def configure_http_error_order_status_response( callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - mock_api.get(regex_url, status=401, callback=callback) + mock_api.post(regex_url, status=401, callback=callback) return url def configure_partially_filled_order_status_response( @@ -577,7 +582,7 @@ def configure_partially_filled_order_status_response( url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_status_request_partially_filled_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_order_not_found_error_order_status_response( @@ -587,7 +592,7 @@ def configure_order_not_found_error_order_status_response( url = web_utils.private_rest_url(CONSTANTS.QUERY_ORDERS_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = {"code": -2013, "msg": "Order does not exist."} - mock_api.get(regex_url, body=json.dumps(response), status=400, callback=callback) + mock_api.post(regex_url, body=json.dumps(response), status=400, callback=callback) return [url] def configure_partial_fill_trade_response( @@ -598,7 +603,7 @@ def configure_partial_fill_trade_response( url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") response = self._order_fills_request_partial_fill_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def configure_full_fill_trade_response( @@ -609,7 +614,7 @@ def configure_full_fill_trade_response( url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) regex_url = re.compile(url + r"\?.*") response = self._order_fills_request_full_fill_mock_response(order=order) - mock_api.get(regex_url, body=json.dumps(response), callback=callback) + mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url def order_event_for_new_order_websocket_update(self, order: InFlightOrder): @@ -624,7 +629,7 @@ def order_event_for_new_order_websocket_update(self, order: InFlightOrder): "leverage": "0:1", "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", "ordertype": "limit", - "pair": convert_to_exchange_trading_pair(self.trading_pair, '/'), + "pair": self.ws_ex_trading_pairs, "price": str(order.price), "price2": "0.00000", "type": "sell" @@ -652,79 +657,119 @@ def order_event_for_new_order_websocket_update(self, order: InFlightOrder): ] def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): - return { - "c": "spot@private.orders.v3.api", - "d": { - "A": 8.0, - "O": 1661938138000, - "S": 1, - "V": 10, - "a": 8, - "c": order.client_order_id, - "i": order.exchange_order_id, - "m": 0, - "o": 1, - "p": order.price, - "s": 4, - "v": order.amount, - "ap": 0, - "cv": 0, - "ca": 0 - }, - "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "t": 1499405658657 - } + return [ + [ + { + order.exchange_order_id: { + "avg_price": "34.50000", + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/EUR", + "price": "34.50000", + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "34.50000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "canceled", + "stopprice": "0.000000", + "userref": 0, + "vol": "10.00345345", + "vol_exec": "0.00000000" + } + } + ], + "openOrders", + { + "sequence": 234 + } + ] def order_event_for_full_fill_websocket_update(self, order: InFlightOrder): - return { - "c": "spot@private.orders.v3.api", - "d": { - "A": 8.0, - "O": 1661938138000, - "S": 1, - "V": 10, - "a": 8, - "c": order.client_order_id, - "i": order.exchange_order_id, - "m": 0, - "o": 1, - "p": order.price, - "s": 2, - "v": order.amount, - "ap": 0, - "cv": 0, - "ca": 0 - }, - "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "t": 1499405658657 - } + return [ + [ + { + order.exchange_order_id: { + "avg_price": "34.50000", + "cost": "0.00000", + "descr": { + "close": "", + "leverage": "0:1", + "order": "sell 10.00345345 XBT/EUR @ limit 34.50000 with 0:1 leverage", + "ordertype": "limit", + "pair": "XBT/EUR", + "price": order.price, + "price2": "0.00000", + "type": "sell" + }, + "expiretm": "0.000000", + "fee": "0.00000", + "limitprice": "34.50000", + "misc": "", + "oflags": "fcib", + "opentm": "0.000000", + "refid": "OKIVMP-5GVZN-Z2D2UA", + "starttm": "0.000000", + "status": "closed", + "stopprice": "0.000000", + "userref": 0, + "vol": order.amount, + "vol_exec": "0.00000000" + } + } + ], + "openOrders", + { + "sequence": 234 + } + ] def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): - return { - "c": "spot@private.deals.v3.api", - "d": { - "p": order.price, - "v": order.amount, - "a": order.price * order.amount, - "S": 1, - "T": 1678901086198, - "t": "5bbb6ad8b4474570b155610e3960cd", - "c": order.client_order_id, - "i": order.exchange_order_id, - "m": 0, - "st": 0, - "n": Decimal(self.expected_fill_fee.flat_fees[0].amount), - "N": self.quote_asset - }, - "s": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "t": 1661938980285 - } + return [ + [ + { + self.expected_fill_trade_id: { + "cost": "1000000.00000", + "fee": Decimal(self.expected_fill_fee.flat_fees[0].amount), + "margin": "0.00000", + "ordertxid": order.exchange_order_id, + "ordertype": "limit", + "pair": "XBT/EUR", + "postxid": "OGTT3Y-C6I3P-XRI6HX", + "price": order.price, + "time": "1560516023.070651", + "type": "sell", + "vol": order.amount + } + } + ], + "ownTrades", + { + "sequence": 2948 + } + ] @aioresponses() @patch("hummingbot.connector.time_synchronizer.TimeSynchronizer._current_seconds_counter") def test_update_time_synchronizer_successfully(self, mock_api, seconds_counter_mock): pass + @aioresponses() + def test_cancel_order_not_found_in_the_exchange(self, mock_api): + # Disabling this test because the connector has not been updated yet to validate + # order not found during cancellation (check _is_order_not_found_during_cancelation_error) + pass + def test_user_stream_balance_update(self): pass @@ -737,112 +782,8 @@ def test_update_time_synchronizer_raises_cancelled_error(self, mock_api): pass @aioresponses() - def test_update_order_fills_from_trades_triggers_filled_event(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - - self.exchange.start_tracking_order( - order_id="OID1", - exchange_order_id="100234", - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal("10000"), - amount=Decimal("1"), - userref=1, - - ) - order = self.exchange.in_flight_orders["OID1"] - - url = web_utils.private_rest_url(CONSTANTS.QUERY_TRADES_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - - trade_fill = { - "error": [], - "result": { - 28457: { - "ordertxid": order.exchange_order_id, - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(self.expected_partial_fill_price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(self.expected_partial_fill_amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" - } - } - } - - trade_fill_non_tracked_order = { - "error": [], - "result": { - 30000: { - "ordertxid": 9999, - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(self.expected_partial_fill_price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(self.expected_partial_fill_amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" - } - } - } - - mock_response = [trade_fill, trade_fill_non_tracked_order] - mock_api.get(regex_url, body=json.dumps(mock_response)) - - self.exchange.add_exchange_order_ids_from_market_recorder( - {str(trade_fill_non_tracked_order["orderId"]): "OID99"}) - - self.async_run_with_timeout(self.exchange._update_order_fills_from_trades()) - - request = self._all_executed_requests(mock_api, url)[0] - self.validate_auth_credentials_present(request) - - 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(Decimal(trade_fill["price"]), fill_event.price) - self.assertEqual(Decimal(trade_fill["vol"]), fill_event.amount) - self.assertEqual(0.0, fill_event.trade_fee.percent) - self.assertEqual([TokenAmount(self.quote_asset, Decimal(trade_fill["fee"]))], - fill_event.trade_fee.flat_fees) - - fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1] - self.assertEqual(float(trade_fill_non_tracked_order["time"]) * 1e-3, fill_event.timestamp) - self.assertEqual("OID99", fill_event.order_id) - self.assertEqual(self.trading_pair, fill_event.trading_pair) - self.assertEqual(TradeType.BUY, fill_event.trade_type) - self.assertEqual(OrderType.LIMIT, fill_event.order_type) - self.assertEqual(Decimal(trade_fill_non_tracked_order["price"]), fill_event.price) - self.assertEqual(Decimal(trade_fill_non_tracked_order["vol"]), fill_event.amount) - self.assertEqual(0.0, fill_event.trade_fee.percent) - self.assertEqual([ - TokenAmount( - self.quote_asset, - Decimal(trade_fill_non_tracked_order["fee"]))], - fill_event.trade_fee.flat_fees) - self.assertTrue(self.is_logged( - "INFO", - f"Recreating missing trade in TradeFill: {trade_fill_non_tracked_order}" - )) + def test_update_order_status_when_order_has_not_changed_and_one_partial_fill(self, mock_api): + pass @aioresponses() def test_update_order_status_when_failed(self, mock_api): @@ -858,7 +799,7 @@ def test_update_order_status_when_failed(self, mock_api): trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), - userref=1, + userref=0, ) order = self.exchange.in_flight_orders["OID1"] @@ -868,32 +809,30 @@ def test_update_order_status_when_failed(self, mock_api): order_status = { "error": [], "result": { - "open": { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "open", - "opentm": 1499827319.559, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": "1.0", - "vol_exec": "0.0", - "cost": "11253.7", - "fee": "0.00000", - "price": "10000.0", - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "expired", + "opentm": 1499827319.559, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": "1.0", + "vol_exec": "0.0", + "cost": "11253.7", + "fee": "0.00000", + "price": "10000.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } } } mock_response = order_status - mock_api.get(regex_url, body=json.dumps(mock_response)) + mock_api.post(regex_url, body=json.dumps(mock_response)) self.async_run_with_timeout(self.exchange._update_order_status()) @@ -911,7 +850,7 @@ def test_update_order_status_when_failed(self, mock_api): self.is_logged( "INFO", f"Order {order.client_order_id} has failed. Order Update: OrderUpdate(trading_pair='{self.trading_pair}'," - f" update_timestamp={order_status['updateTime'] * 1e-3}, new_state={repr(OrderState.FAILED)}, " + f" update_timestamp={self.exchange.current_timestamp}, new_state={repr(OrderState.FAILED)}, " f"client_order_id='{order.client_order_id}', exchange_order_id='{order.exchange_order_id}', " "misc_updates=None)") ) @@ -950,72 +889,203 @@ def test_client_order_id_on_order(self, mocked_nonce): self.assertEqual(result, expected_client_order_id) - @aioresponses() - def test_place_order_manage_server_overloaded_error_unkown_order(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) - url = web_utils.private_rest_url(CONSTANTS.ADD_ORDER_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - mock_response = {"code": -1003, "msg": "Unknown error, please check your request or try again later."} - mock_api.post(regex_url, body=json.dumps(mock_response), status=503) + def _validate_auth_credentials_taking_parameters_from_argument(self, + request_call_tuple: RequestCall, + params: Dict[str, Any]): + self.assertIn("nonce", params) + request_headers = request_call_tuple.kwargs["headers"] + self.assertIn("API-Sign", request_headers) + self.assertIn("API-Key", request_headers) + self.assertEqual("someKey", request_headers["API-Key"]) - o_id, transact_time = self.async_run_with_timeout(self.exchange._place_order( - order_id="test_order_id", + def test_restore_tracking_states_only_registers_open_orders(self): + orders = [] + orders.append(KrakenInFlightOrder( + client_order_id=self.client_order_id_prefix + "1", + exchange_order_id=str(self.expected_exchange_order_id), trading_pair=self.trading_pair, - amount=Decimal("1"), + order_type=OrderType.LIMIT, trade_type=TradeType.BUY, + amount=Decimal("1000.0"), + price=Decimal("1.0"), + creation_timestamp=1640001112.223, + userref=self._userref + )) + orders.append(KrakenInFlightOrder( + client_order_id=self.client_order_id_prefix + "2", + exchange_order_id=self.exchange_order_id_prefix + "2", + trading_pair=self.trading_pair, order_type=OrderType.LIMIT, - price=Decimal("2"), + trade_type=TradeType.BUY, + amount=Decimal("1000.0"), + price=Decimal("1.0"), + creation_timestamp=1640001112.223, + initial_state=OrderState.CANCELED, + userref=self._userref + + )) + orders.append(KrakenInFlightOrder( + client_order_id=self.client_order_id_prefix + "3", + exchange_order_id=self.exchange_order_id_prefix + "3", + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + amount=Decimal("1000.0"), + price=Decimal("1.0"), + creation_timestamp=1640001112.223, + initial_state=OrderState.FILLED, + userref=self._userref + + )) + orders.append(KrakenInFlightOrder( + client_order_id=self.client_order_id_prefix + "4", + exchange_order_id=self.exchange_order_id_prefix + "4", + trading_pair=self.trading_pair, + order_type=OrderType.LIMIT, + trade_type=TradeType.BUY, + amount=Decimal("1000.0"), + price=Decimal("1.0"), + creation_timestamp=1640001112.223, + initial_state=OrderState.FAILED, + userref=self._userref + )) - self.assertEqual(o_id, "UNKNOWN") + + tracking_states = {order.client_order_id: order.to_json() for order in orders} + + self.exchange.restore_tracking_states(tracking_states) + + self.assertIn(self.client_order_id_prefix + "1", self.exchange.in_flight_orders) + self.assertNotIn(self.client_order_id_prefix + "2", self.exchange.in_flight_orders) + self.assertNotIn(self.client_order_id_prefix + "3", self.exchange.in_flight_orders) + self.assertNotIn(self.client_order_id_prefix + "4", self.exchange.in_flight_orders) + + def get_asset_pairs_mock(self) -> Dict: + asset_pairs = { + "error": [], + "result": { + f"X{self.base_asset}{self.quote_asset}": { + "altname": f"{self.base_asset}{self.quote_asset}", + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": f"{self.base_asset}", + "aclass_quote": "currency", + "quote": f"{self.quote_asset}", + "lot": "unit", + "pair_decimals": 5, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [ + 2, + 3, + ], + "leverage_sell": [ + 2, + 3, + ], + "fees": [ + [ + 0, + 0.26 + ], + [ + 50000, + 0.24 + ], + ], + "fees_maker": [ + [ + 0, + 0.16 + ], + [ + 50000, + 0.14 + ], + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.005" + }, + } + } + return asset_pairs + + def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: + balances = { + "error": [], + "result": { + self.base_asset: str(base_asset_balance), + self.quote_asset: str(quote_asset_balance), + "USDT": "171288.6158", + } + } + return balances + + def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: + open_orders = { + "error": [], + "result": { + "open": { + "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), + } + } + } + return open_orders + + def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: + order_status = { + "refid": None, + "userref": 0, + "status": status, + "opentm": 1616666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": { + "pair": f"{self.base_asset}{self.quote_asset}", + "type": order_type, + "ordertype": "limit", + "price": str(price), + "price2": "0", + "leverage": "none", + "order": f"buy {quantity} {self.base_asset}{self.quote_asset} @ limit {price}", + "close": "" + }, + "vol": str(quantity), + "vol_exec": "0", + "cost": str(price * quantity), + "fee": "0.00000", + "price": str(price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [ + "TCCCTY-WE2O6-P3NB37" + ] + } + return order_status @aioresponses() - def test_place_order_manage_server_overloaded_error_failure(self, mock_api): - self.exchange._set_current_timestamp(1640780000) - self.exchange._last_poll_timestamp = (self.exchange.current_timestamp - - self.exchange.UPDATE_ORDER_STATUS_MIN_INTERVAL - 1) + def test_update_balances(self, mocked_api): + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" + resp = self.get_asset_pairs_mock() + mocked_api.get(url, body=json.dumps(resp)) - url = web_utils.private_rest_url(CONSTANTS.ADD_ORDER_PATH_URL) + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.BALANCE_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - mock_response = {"code": -1003, "msg": "Service Unavailable."} - mock_api.post(regex_url, body=json.dumps(mock_response), status=503) - - self.assertRaises( - IOError, - self.async_run_with_timeout, - self.exchange._place_order( - order_id="test_order_id", - trading_pair=self.trading_pair, - amount=Decimal("1"), - trade_type=TradeType.BUY, - order_type=OrderType.LIMIT, - price=Decimal("2"), - )) - - mock_response = {"code": -1003, "msg": "Internal error; unable to process your request. Please try again."} - mock_api.post(regex_url, body=json.dumps(mock_response), status=503) - - self.assertRaises( - IOError, - self.async_run_with_timeout, - self.exchange._place_order( - order_id="test_order_id", - trading_pair=self.trading_pair, - amount=Decimal("1"), - trade_type=TradeType.BUY, - order_type=OrderType.LIMIT, - price=Decimal("2"), - )) + resp = self.get_balances_mock(base_asset_balance=10, quote_asset_balance=20) + mocked_api.post(regex_url, body=json.dumps(resp)) - def _validate_auth_credentials_taking_parameters_from_argument(self, - request_call_tuple: RequestCall, - params: Dict[str, Any]): - self.assertIn("nonce", params) - request_headers = request_call_tuple.kwargs["headers"] - self.assertIn("API-Sign", request_headers) - self.assertIn("API-Key", request_headers) - self.assertEqual("testAPIKey", request_headers["API-Key"]) + url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") + mocked_api.post(regex_url, body=json.dumps(resp)) + + self.async_run_with_timeout(self.exchange._update_balances()) + + self.assertEqual(self.exchange.available_balances[self.quote_asset], Decimal("171286.6158")) def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return { @@ -1026,39 +1096,54 @@ def _order_cancelation_request_successful_mock_response(self, order: InFlightOrd } def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: - return \ - { - "error": [], - "result": { - "open": { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "closed", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": str(order.amount), - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } - } + return { + "error": [], + "result": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "closed", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } } + } def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { "error": [], "result": { - "count": 1 + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "canceled", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } } } @@ -1066,26 +1151,24 @@ def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: return { "error": [], "result": { - "open": { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "open", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": "0", - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } } } @@ -1094,26 +1177,24 @@ def _order_status_request_partially_filled_mock_response(self, order: InFlightOr return { "error": [], "result": { - "open": { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "open", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": str(order.amount / 2), - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount / 2), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } } } @@ -1123,7 +1204,7 @@ def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): "error": [], "result": { self.expected_fill_trade_id: { - "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "ordertxid": order.exchange_order_id, "postxid": "TKH2SE-M7IF5-CFI7LT", "pair": "XXBTZUSD", "time": 1499865549.590, @@ -1146,7 +1227,7 @@ def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): "error": [], "result": { self.expected_fill_trade_id: { - "ordertxid": "OQCLML-BW3P3-BUCMWZ", + "ordertxid": order.exchange_order_id, "postxid": "TKH2SE-M7IF5-CFI7LT", "pair": "XXBTZUSD", "time": 1499865549.590, From ab82c3ea38ed708df623049d019562b881b5c5d3 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 07:19:43 +0800 Subject: [PATCH 14/34] format code --- .../connector/exchange/kraken_v1/__init__.py | 0 .../kraken_api_order_book_data_source.py | 290 ----- .../kraken_api_user_stream_data_source.py | 126 -- .../exchange/kraken_v1/kraken_auth.py | 49 - .../exchange/kraken_v1/kraken_constants.py | 100 -- .../exchange/kraken_v1/kraken_exchange.pxd | 43 - .../exchange/kraken_v1/kraken_exchange.pyx | 1160 ----------------- .../kraken_v1/kraken_in_flight_order.pxd | 6 - .../kraken_v1/kraken_in_flight_order.pyx | 101 -- .../exchange/kraken_v1/kraken_order_book.pxd | 4 - .../exchange/kraken_v1/kraken_order_book.pyx | 86 -- .../kraken_v1/kraken_order_book_tracker.py | 98 -- .../kraken_v1/kraken_tracking_nonce.py | 10 - .../kraken_v1/kraken_user_stream_tracker.py | 52 - .../exchange/kraken_v1/kraken_utils.py | 217 --- .../test_kraken_api_order_book_data_source.py | 4 +- ...test_kraken_api_user_stream_data_source.py | 6 +- .../exchange/kraken/test_kraken_auth.py | 3 +- .../exchange/kraken/test_kraken_exchange.py | 16 +- .../kraken/test_kraken_in_flight_order.py | 4 +- .../exchange/kraken/test_kraken_order_book.py | 2 +- .../exchange/kraken/test_kraken_utils.py | 5 +- .../connector/exchange/kraken_v1/__init__.py | 0 .../test_kraken_api_order_book_data_source.py | 271 ---- ...test_kraken_api_user_stream_data_source.py | 138 -- .../kraken_v1/test_kraken_exchange.py | 454 ------- .../kraken_v1/test_kraken_in_flight_order.py | 90 -- 27 files changed, 24 insertions(+), 3311 deletions(-) delete mode 100644 hummingbot/connector/exchange/kraken_v1/__init__.py delete mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py delete mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py delete mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_auth.py delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_constants.py delete mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd delete mode 100755 hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py delete mode 100644 hummingbot/connector/exchange/kraken_v1/kraken_utils.py delete mode 100644 test/hummingbot/connector/exchange/kraken_v1/__init__.py delete mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py delete mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py delete mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py delete mode 100644 test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py diff --git a/hummingbot/connector/exchange/kraken_v1/__init__.py b/hummingbot/connector/exchange/kraken_v1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py deleted file mode 100755 index 7c165667fd..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_api_order_book_data_source.py +++ /dev/null @@ -1,290 +0,0 @@ -import asyncio -import logging -import time -from typing import Any, Dict, List, Optional - -import pandas as pd - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook -from hummingbot.connector.exchange.kraken.kraken_utils import ( - build_api_factory, - build_rate_limits_by_tier, - convert_from_exchange_trading_pair, - convert_to_exchange_trading_pair, -) -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.data_type.order_book import OrderBook -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.utils.async_utils import safe_gather -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest -from hummingbot.core.web_assistant.rest_assistant import RESTAssistant -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory -from hummingbot.core.web_assistant.ws_assistant import WSAssistant -from hummingbot.logger import HummingbotLogger - - -class KrakenAPIOrderBookDataSource(OrderBookTrackerDataSource): - MESSAGE_TIMEOUT = 30.0 - PING_TIMEOUT = 10.0 - - _kraobds_logger: Optional[HummingbotLogger] = None - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._kraobds_logger is None: - cls._kraobds_logger = logging.getLogger(__name__) - return cls._kraobds_logger - - def __init__(self, - throttler: Optional[AsyncThrottler] = None, - trading_pairs: List[str] = None, - api_factory: Optional[WebAssistantsFactory] = None): - super().__init__(trading_pairs) - self._throttler = throttler or self._get_throttler_instance() - self._api_factory = api_factory or build_api_factory(throttler=throttler) - self._rest_assistant = None - self._ws_assistant = None - self._order_book_create_function = lambda: OrderBook() - - @classmethod - def _get_throttler_instance(cls) -> AsyncThrottler: - throttler = AsyncThrottler(build_rate_limits_by_tier()) - return throttler - - async def _get_rest_assistant(self) -> RESTAssistant: - if self._rest_assistant is None: - self._rest_assistant = await self._api_factory.get_rest_assistant() - return self._rest_assistant - - @classmethod - async def get_last_traded_prices( - cls, trading_pairs: List[str], throttler: Optional[AsyncThrottler] = None - ) -> Dict[str, float]: - throttler = throttler or cls._get_throttler_instance() - tasks = [cls._get_last_traded_price(t_pair, throttler) for t_pair in trading_pairs] - results = await safe_gather(*tasks) - return {t_pair: result for t_pair, result in zip(trading_pairs, results)} - - @classmethod - async def _get_last_traded_price(cls, trading_pair: str, throttler: AsyncThrottler) -> float: - url = ( - f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" - f"?pair={convert_to_exchange_trading_pair(trading_pair)}" - ) - - request = RESTRequest( - method=RESTMethod.GET, - url=url - ) - rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - - async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL): - resp = await rest_assistant.call(request) - resp_json = await resp.json() - record = list(resp_json["result"].values())[0] - return float(record["c"][0]) - - @classmethod - async def get_snapshot( - cls, - rest_assistant: RESTAssistant, - trading_pair: str, - limit: int = 1000, - throttler: Optional[AsyncThrottler] = None, - ) -> Dict[str, Any]: - throttler = throttler or cls._get_throttler_instance() - original_trading_pair: str = trading_pair - if limit != 0: - params = { - "count": str(limit), - "pair": convert_to_exchange_trading_pair(trading_pair) - } - else: - params = {"pair": convert_to_exchange_trading_pair(trading_pair)} - async with throttler.execute_task(CONSTANTS.SNAPSHOT_PATH_URL): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" - - request = RESTRequest( - method=RESTMethod.GET, - url=url, - params=params - ) - - response = await rest_assistant.call(request) - - if response.status != 200: - raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " - f"HTTP status is {response.status}.") - response_json = await response.json() - if len(response_json["error"]) > 0: - raise IOError(f"Error fetching Kraken market snapshot for {original_trading_pair}. " - f"Error is {response_json['error']}.") - data: Dict[str, Any] = next(iter(response_json["result"].values())) - data = {"trading_pair": trading_pair, **data} - data["latest_update"] = max([*map(lambda x: x[2], data["bids"] + data["asks"])], default=0.) - - return data - - async def get_new_order_book(self, trading_pair: str) -> OrderBook: - rest_assistant = await self._get_rest_assistant() - snapshot: Dict[str, Any] = await self.get_snapshot( - rest_assistant, trading_pair, limit=1000, throttler=self._throttler - ) - snapshot_timestamp: float = time.time() - snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( - snapshot, - snapshot_timestamp, - metadata={"trading_pair": trading_pair} - ) - order_book: OrderBook = self.order_book_create_function() - order_book.apply_snapshot(snapshot_msg.bids, snapshot_msg.asks, snapshot_msg.update_id) - return order_book - - @classmethod - async def fetch_trading_pairs(cls, throttler: Optional[AsyncThrottler] = None) -> List[str]: - throttler = throttler or cls._get_throttler_instance() - try: - async with throttler.execute_task(CONSTANTS.ASSET_PAIRS_PATH_URL): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - request = RESTRequest( - method=RESTMethod.GET, - url=url - ) - rest_assistant = await build_api_factory(throttler=throttler).get_rest_assistant() - response = await rest_assistant.call(request, timeout=5) - - if response.status == 200: - data: Dict[str, Any] = await response.json() - raw_pairs = data.get("result", []) - converted_pairs: List[str] = [] - for pair, details in raw_pairs.items(): - if "." not in pair: - try: - wsname = details["wsname"] # pair in format BASE/QUOTE - converted_pairs.append(convert_from_exchange_trading_pair(wsname)) - except IOError: - pass - return [item for item in converted_pairs] - except Exception: - pass - # Do nothing if the request fails -- there will be no autocomplete for kraken trading pairs - return [] - - async def listen_for_trades(self, ev_loop: asyncio.AbstractEventLoop, output: asyncio.Queue): - while True: - try: - ws_message: str = await self.get_ws_subscription_message("trade") - - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) - - await ws.send(ws_message) - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - trades = [ - {"pair": convert_from_exchange_trading_pair(msg[-1]), "trade": trade} - for trade in msg[1] - ] - for trade in trades: - trade_msg: OrderBookMessage = KrakenOrderBook.trade_message_from_exchange(trade) - output.put_nowait(trade_msg) - ws.disconnect() - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", - exc_info=True) - await asyncio.sleep(30.0) - - async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): - while True: - try: - ws_message: str = await self.get_ws_subscription_message("book") - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_URL, ping_timeout=self.PING_TIMEOUT) - - await ws.send(ws_message) - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - msg_dict = {"trading_pair": convert_from_exchange_trading_pair(msg[-1]), - "asks": msg[1].get("a", []) or msg[1].get("as", []) or [], - "bids": msg[1].get("b", []) or msg[1].get("bs", []) or []} - msg_dict["update_id"] = max( - [*map(lambda x: float(x[2]), msg_dict["bids"] + msg_dict["asks"])], default=0. - ) - if "as" in msg[1] and "bs" in msg[1]: - order_book_message: OrderBookMessage = ( - KrakenOrderBook.snapshot_ws_message_from_exchange(msg_dict, time.time()) - ) - else: - order_book_message: OrderBookMessage = KrakenOrderBook.diff_message_from_exchange( - msg_dict, time.time()) - output.put_nowait(order_book_message) - ws.disconnect() - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with WebSocket connection. Retrying after 30 seconds...", - exc_info=True) - await asyncio.sleep(30.0) - - async def listen_for_order_book_snapshots(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): - rest_assistant = await self._get_rest_assistant() - while True: - try: - for trading_pair in self._trading_pairs: - try: - snapshot: Dict[str, Any] = await self.get_snapshot( - rest_assistant, trading_pair, throttler=self._throttler - ) - snapshot_timestamp: float = time.time() - snapshot_msg: OrderBookMessage = KrakenOrderBook.snapshot_message_from_exchange( - snapshot, - snapshot_timestamp, - metadata={"trading_pair": trading_pair} - ) - output.put_nowait(snapshot_msg) - self.logger().debug(f"Saved order book snapshot for {trading_pair}") - await asyncio.sleep(5.0) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error. ", exc_info=True) - await asyncio.sleep(5.0) - this_hour: pd.Timestamp = pd.Timestamp.utcnow().replace(minute=0, second=0, microsecond=0) - next_hour: pd.Timestamp = this_hour + pd.Timedelta(hours=1) - delta: float = next_hour.timestamp() - time.time() - await asyncio.sleep(delta) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error. ", exc_info=True) - await asyncio.sleep(5.0) - - async def get_ws_subscription_message(self, subscription_type: str): - trading_pairs: List[str] = [] - for tp in self._trading_pairs: - trading_pairs.append(convert_to_exchange_trading_pair(tp, '/')) - - ws_message: WSJSONRequest = WSJSONRequest({ - "event": "subscribe", - "pair": trading_pairs, - "subscription": {"name": subscription_type, "depth": 1000}}) - - return ws_message - - async def listen_for_subscriptions(self): - """ - Connects to the trade events and order diffs websocket endpoints and listens to the messages sent by the - exchange. Each message is stored in its own queue. - """ - # This connector does not use this base class method and needs a refactoring - pass diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py deleted file mode 100755 index c3484c2d7e..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_api_user_stream_data_source.py +++ /dev/null @@ -1,126 +0,0 @@ -import asyncio -import logging -from typing import Any, Dict, Optional - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook -from hummingbot.connector.exchange.kraken.kraken_utils import build_api_factory -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest, WSJSONRequest -from hummingbot.core.web_assistant.rest_assistant import RESTAssistant -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory -from hummingbot.core.web_assistant.ws_assistant import WSAssistant -from hummingbot.logger import HummingbotLogger - -MESSAGE_TIMEOUT = 3.0 -PING_TIMEOUT = 5.0 - - -class KrakenAPIUserStreamDataSource(UserStreamTrackerDataSource): - - _krausds_logger: Optional[HummingbotLogger] = None - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._krausds_logger is None: - cls._krausds_logger = logging.getLogger(__name__) - return cls._krausds_logger - - def __init__(self, - throttler: AsyncThrottler, - kraken_auth: KrakenAuth, - api_factory: Optional[WebAssistantsFactory] = None): - self._throttler = throttler - self._api_factory = api_factory or build_api_factory(throttler=throttler) - self._rest_assistant = None - self._ws_assistant = None - self._kraken_auth: KrakenAuth = kraken_auth - self._current_auth_token: Optional[str] = None - super().__init__() - - @property - def order_book_class(self): - return KrakenOrderBook - - @property - def last_recv_time(self): - if self._ws_assistant is None: - return 0 - else: - return self._ws_assistant.last_recv_time - - async def _get_rest_assistant(self) -> RESTAssistant: - if self._rest_assistant is None: - self._rest_assistant = await self._api_factory.get_rest_assistant() - return self._rest_assistant - - async def get_auth_token(self) -> str: - api_auth: Dict[str, Any] = self._kraken_auth.generate_auth_dict(uri=CONSTANTS.GET_TOKEN_PATH_URL) - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" - - request = RESTRequest( - method=RESTMethod.POST, - url=url, - headers=api_auth["headers"], - data=api_auth["postDict"] - ) - rest_assistant = await self._get_rest_assistant() - - async with self._throttler.execute_task(CONSTANTS.GET_TOKEN_PATH_URL): - response = await rest_assistant.call(request=request, timeout=100) - if response.status != 200: - raise IOError(f"Error fetching Kraken user stream listen key. HTTP status is {response.status}.") - - try: - response_json: Dict[str, Any] = await response.json() - except Exception: - raise IOError(f"Error parsing data from {url}.") - - err = response_json["error"] - if "EAPI:Invalid nonce" in err: - self.logger().error(f"Invalid nonce error from {url}. " + - "Please ensure your Kraken API key nonce window is at least 10, " + - "and if needed reset your API key.") - raise IOError({"error": response_json}) - - return response_json["result"]["token"] - - async def listen_for_user_stream(self, output: asyncio.Queue): - ws = None - while True: - try: - async with self._throttler.execute_task(CONSTANTS.WS_CONNECTION_LIMIT_ID): - ws: WSAssistant = await self._api_factory.get_ws_assistant() - await ws.connect(ws_url=CONSTANTS.WS_AUTH_URL, ping_timeout=PING_TIMEOUT) - - if self._current_auth_token is None: - self._current_auth_token = await self.get_auth_token() - - for subscription_type in ["openOrders", "ownTrades"]: - subscribe_request: WSJSONRequest = WSJSONRequest({ - "event": "subscribe", - "subscription": { - "name": subscription_type, - "token": self._current_auth_token - } - }) - await ws.send(subscribe_request) - - async for ws_response in ws.iter_messages(): - msg = ws_response.data - if not (type(msg) is dict and "event" in msg.keys() and - msg["event"] in ["heartbeat", "systemStatus", "subscriptionStatus"]): - output.put_nowait(msg) - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error with Kraken WebSocket connection. " - "Retrying after 30 seconds...", exc_info=True) - self._current_auth_token = None - await asyncio.sleep(30.0) - finally: - if ws is not None: - await ws.disconnect() diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_auth.py b/hummingbot/connector/exchange/kraken_v1/kraken_auth.py deleted file mode 100755 index 1359d6d8a3..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_auth.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import ( - Optional, - Dict, - Any -) -import base64 -import hashlib -import hmac -from hummingbot.connector.exchange.kraken.kraken_tracking_nonce import get_tracking_nonce - - -class KrakenAuth: - def __init__(self, api_key: str, secret_key: str): - self.api_key = api_key - self.secret_key = secret_key - - def generate_auth_dict(self, uri: str, data: Optional[Dict[str, str]] = None) -> Dict[str, Any]: - """ - Generates authentication signature and returns it in a dictionary - :return: a dictionary of request info including the request signature and post data - """ - - # Decode API private key from base64 format displayed in account management - api_secret: bytes = base64.b64decode(self.secret_key) - - # Variables (API method, nonce, and POST data) - api_path: bytes = bytes(uri, 'utf-8') - api_nonce: str = get_tracking_nonce() - api_post: str = "nonce=" + api_nonce - - if data is not None: - for key, value in data.items(): - api_post += f"&{key}={value}" - - # Cryptographic hash algorithms - api_sha256: bytes = hashlib.sha256(bytes(api_nonce + api_post, 'utf-8')).digest() - api_hmac: hmac.HMAC = hmac.new(api_secret, api_path + api_sha256, hashlib.sha512) - - # Encode signature into base64 format used in API-Sign value - api_signature: bytes = base64.b64encode(api_hmac.digest()) - - return { - "headers": { - "API-Key": self.api_key, - "API-Sign": str(api_signature, 'utf-8') - }, - "post": api_post, - "postDict": {"nonce": api_nonce, **data} if data is not None else {"nonce": api_nonce} - } diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_constants.py b/hummingbot/connector/exchange/kraken_v1/kraken_constants.py deleted file mode 100644 index f30dca5323..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_constants.py +++ /dev/null @@ -1,100 +0,0 @@ -from enum import Enum -from typing import ( - Dict, - Tuple, -) -from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair - - -class KrakenAPITier(Enum): - """ - Kraken's Private Endpoint Rate Limit Tiers, based on the Account Verification level. - """ - STARTER = "STARTER" - INTERMEDIATE = "INTERMEDIATE" - PRO = "PRO" - - -# Values are calculated by adding the Maxiumum Counter value and the expected count decay(in a minute) of a given tier. -# Reference: -# - API Rate Limits: https://support.kraken.com/hc/en-us/articles/206548367-What-are-the-API-rate-limits -# - Matching Engine Limits: https://support.kraken.com/hc/en-us/articles/360045239571 -STARTER_PRIVATE_ENDPOINT_LIMIT = 15 + 20 -STARTER_MATCHING_ENGINE_LIMIT = 60 + 60 -INTERMEDIATE_PRIVATE_ENDPOINT_LIMIT = 20 + 30 -INTERMEDIATE_MATCHING_ENGINE_LIMIT = 125 + 140 -PRO_PRIVATE_ENDPOINT_LIMIT = 20 + 60 -PRO_MATCHING_ENGINE_LIMIT = 180 + 225 - -KRAKEN_TIER_LIMITS: Dict[KrakenAPITier, Tuple[int, int]] = { - KrakenAPITier.STARTER: (STARTER_PRIVATE_ENDPOINT_LIMIT, STARTER_MATCHING_ENGINE_LIMIT), - KrakenAPITier.INTERMEDIATE: (INTERMEDIATE_PRIVATE_ENDPOINT_LIMIT, INTERMEDIATE_MATCHING_ENGINE_LIMIT), - KrakenAPITier.PRO: (PRO_PRIVATE_ENDPOINT_LIMIT, PRO_MATCHING_ENGINE_LIMIT), -} - -KRAKEN_TO_HB_MAP = { - "XBT": "BTC", - "XDG": "DOGE", -} - -BASE_URL = "https://api.kraken.com" -TICKER_PATH_URL = "/0/public/Ticker" -SNAPSHOT_PATH_URL = "/0/public/Depth" -ASSET_PAIRS_PATH_URL = "/0/public/AssetPairs" -TIME_PATH_URL = "/0/public/Time" -GET_TOKEN_PATH_URL = "/0/private/GetWebSocketsToken" -ADD_ORDER_PATH_URL = "/0/private/AddOrder" -CANCEL_ORDER_PATH_URL = "/0/private/CancelOrder" -BALANCE_PATH_URL = "/0/private/Balance" -OPEN_ORDERS_PATH_URL = "/0/private/OpenOrders" -QUERY_ORDERS_PATH_URL = "/0/private/QueryOrders" - -WS_URL = "wss://ws.kraken.com" -WS_AUTH_URL = "wss://ws-auth.kraken.com/" - -PUBLIC_ENDPOINT_LIMIT_ID = "PublicEndpointLimitID" -PUBLIC_ENDPOINT_LIMIT = 1 -PUBLIC_ENDPOINT_LIMIT_INTERVAL = 1 -PRIVATE_ENDPOINT_LIMIT_ID = "PrivateEndpointLimitID" -PRIVATE_ENDPOINT_LIMIT_INTERVAL = 60 -MATCHING_ENGINE_LIMIT_ID = "MatchingEngineLimitID" -MATCHING_ENGINE_LIMIT_INTERVAL = 60 -WS_CONNECTION_LIMIT_ID = "WSConnectionLimitID" - -PUBLIC_API_LIMITS = [ - # Public API Pool - RateLimit( - limit_id=PUBLIC_ENDPOINT_LIMIT_ID, - limit=PUBLIC_ENDPOINT_LIMIT, - time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, - ), - # Public Endpoints - RateLimit( - limit_id=SNAPSHOT_PATH_URL, - limit=PUBLIC_ENDPOINT_LIMIT, - time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=ASSET_PAIRS_PATH_URL, - limit=PUBLIC_ENDPOINT_LIMIT, - time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=TICKER_PATH_URL, - limit=PUBLIC_ENDPOINT_LIMIT, - time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=TIME_PATH_URL, - limit=PUBLIC_ENDPOINT_LIMIT, - time_interval=PUBLIC_ENDPOINT_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(PUBLIC_ENDPOINT_LIMIT_ID)], - ), - # WebSocket Connection Limit - RateLimit(limit_id=WS_CONNECTION_LIMIT_ID, - limit=150, - time_interval=60 * 10), -] diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd deleted file mode 100755 index accc4a92c1..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pxd +++ /dev/null @@ -1,43 +0,0 @@ -from hummingbot.connector.exchange_base cimport ExchangeBase -from hummingbot.core.data_type.transaction_tracker cimport TransactionTracker -from libc.stdint cimport int32_t - - -cdef class KrakenExchange(ExchangeBase): - cdef: - object _user_stream_tracker - object _ev_loop - object _poll_notifier - double _last_timestamp - double _poll_interval - double _last_pull_timestamp - dict _in_flight_orders - dict _order_not_found_records - TransactionTracker _tx_tracker - dict _trading_rules - dict _trade_fees - double _last_update_trade_fees_timestamp - public object _status_polling_task - public object _user_stream_event_listener_task - public object _user_stream_tracker_task - public object _trading_rules_polling_task - object _async_scheduler - object _set_server_time_offset_task - public object _kraken_auth - object _api_factory - object _rest_assistant - dict _asset_pairs - int32_t _last_userref - object _throttler - object _kraken_api_tier - - cdef c_did_timeout_tx(self, str tracking_id) - cdef c_start_tracking_order(self, - str order_id, - str exchange_order_id, - str trading_pair, - object trade_type, - object price, - object amount, - object order_type, - int userref) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx deleted file mode 100755 index e383426a32..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_exchange.pyx +++ /dev/null @@ -1,1160 +0,0 @@ -import asyncio -import copy -import logging -import re -from collections import defaultdict -from decimal import Decimal -from typing import ( - Any, - AsyncIterable, - Dict, - List, - Optional, - TYPE_CHECKING, -) - -from async_timeout import timeout -from libc.stdint cimport int32_t, int64_t - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.connector.exchange.kraken.kraken_in_flight_order import ( - KrakenInFlightOrder, - KrakenInFlightOrderNotCreated, -) -from hummingbot.connector.exchange.kraken.kraken_order_book_tracker import KrakenOrderBookTracker -from hummingbot.connector.exchange.kraken.kraken_user_stream_tracker import KrakenUserStreamTracker -from hummingbot.connector.exchange.kraken.kraken_utils import ( - build_api_factory, - build_rate_limits_by_tier, - convert_from_exchange_symbol, - convert_from_exchange_trading_pair, - convert_to_exchange_trading_pair, - is_dark_pool, - split_to_base_quote, -) -from hummingbot.connector.exchange_base import ExchangeBase -from hummingbot.connector.trading_rule cimport TradingRule -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.clock cimport Clock -from hummingbot.core.data_type.cancellation_result import CancellationResult -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 cimport OrderBook -from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TokenAmount -from hummingbot.core.data_type.transaction_tracker import TransactionTracker -from hummingbot.core.event.events import ( - BuyOrderCompletedEvent, - BuyOrderCreatedEvent, - MarketEvent, - MarketOrderFailureEvent, - MarketTransactionFailureEvent, - OrderCancelledEvent, - OrderFilledEvent, - SellOrderCompletedEvent, - SellOrderCreatedEvent, -) -from hummingbot.core.network_iterator import NetworkStatus -from hummingbot.core.utils.async_call_scheduler import AsyncCallScheduler -from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather -from hummingbot.core.utils.tracking_nonce import get_tracking_nonce -from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest -from hummingbot.core.web_assistant.rest_assistant import RESTAssistant -from hummingbot.logger import HummingbotLogger - -if TYPE_CHECKING: - from hummingbot.client.config.config_helpers import ClientConfigAdapter - -s_logger = None -s_decimal_0 = Decimal(0) -s_decimal_NaN = Decimal("NaN") - - -cdef class KrakenExchangeTransactionTracker(TransactionTracker): - cdef: - KrakenExchange _owner - - def __init__(self, owner: KrakenExchange): - super().__init__() - self._owner = owner - - cdef c_did_timeout_tx(self, str tx_id): - TransactionTracker.c_did_timeout_tx(self, tx_id) - self._owner.c_did_timeout_tx(tx_id) - - -cdef class KrakenExchange(ExchangeBase): - MARKET_RECEIVED_ASSET_EVENT_TAG = MarketEvent.ReceivedAsset.value - MARKET_BUY_ORDER_COMPLETED_EVENT_TAG = MarketEvent.BuyOrderCompleted.value - MARKET_SELL_ORDER_COMPLETED_EVENT_TAG = MarketEvent.SellOrderCompleted.value - MARKET_ORDER_CANCELED_EVENT_TAG = MarketEvent.OrderCancelled.value - MARKET_TRANSACTION_FAILURE_EVENT_TAG = MarketEvent.TransactionFailure.value - MARKET_ORDER_FAILURE_EVENT_TAG = MarketEvent.OrderFailure.value - MARKET_ORDER_FILLED_EVENT_TAG = MarketEvent.OrderFilled.value - MARKET_BUY_ORDER_CREATED_EVENT_TAG = MarketEvent.BuyOrderCreated.value - MARKET_SELL_ORDER_CREATED_EVENT_TAG = MarketEvent.SellOrderCreated.value - - REQUEST_ATTEMPTS = 5 - - ORDER_NOT_EXIST_CONFIRMATION_COUNT = 3 - - @classmethod - def logger(cls) -> HummingbotLogger: - global s_logger - if s_logger is None: - s_logger = logging.getLogger(__name__) - return s_logger - - def __init__(self, - client_config_map: "ClientConfigAdapter", - kraken_api_key: str, - kraken_secret_key: str, - poll_interval: float = 30.0, - trading_pairs: Optional[List[str]] = None, - trading_required: bool = True, - kraken_api_tier: str = "starter"): - - super().__init__(client_config_map) - self._trading_required = trading_required - self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) - self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) - self._api_factory = build_api_factory(throttler=self._throttler) - self._rest_assistant = None - self._set_order_book_tracker(KrakenOrderBookTracker(trading_pairs=trading_pairs, throttler=self._throttler)) - self._kraken_auth = KrakenAuth(kraken_api_key, kraken_secret_key) - self._user_stream_tracker = KrakenUserStreamTracker(self._throttler, self._kraken_auth, self._api_factory) - self._ev_loop = asyncio.get_event_loop() - self._poll_notifier = asyncio.Event() - self._last_timestamp = 0 - self._poll_interval = poll_interval - self._in_flight_orders = {} # Dict[client_order_id:str, KrakenInFlightOrder] - self._order_not_found_records = {} # Dict[client_order_id:str, count:int] - self._tx_tracker = KrakenExchangeTransactionTracker(self) - self._trading_rules = {} # Dict[trading_pair:str, TradingRule] - self._trade_fees = {} # Dict[trading_pair:str, (maker_fee_percent:Decimal, taken_fee_percent:Decimal)] - self._last_update_trade_fees_timestamp = 0 - self._status_polling_task = None - self._user_stream_tracker_task = None - self._user_stream_event_listener_task = None - self._trading_rules_polling_task = None - self._async_scheduler = AsyncCallScheduler(call_interval=0.5) - self._last_pull_timestamp = 0 - self._asset_pairs = {} - self._last_userref = 0 - self._real_time_balance_update = False - - @property - def name(self) -> str: - return "kraken" - - @property - def order_books(self) -> Dict[str, OrderBook]: - return self.order_book_tracker.order_books - - @property - def kraken_auth(self) -> KrakenAuth: - return self._kraken_auth - - @property - def trading_rules(self) -> Dict[str, TradingRule]: - return self._trading_rules - - @property - def in_flight_orders(self) -> Dict[str, KrakenInFlightOrder]: - return self._in_flight_orders - - @property - def limit_orders(self) -> List[LimitOrder]: - return [ - in_flight_order.to_limit_order() - for in_flight_order in self._in_flight_orders.values() - ] - - @property - def tracking_states(self) -> Dict[str, Any]: - return { - order_id: value.to_json() - for order_id, value in self._in_flight_orders.items() - } - - def restore_tracking_states(self, saved_states: Dict[str, Any]): - in_flight_orders: Dict[str, KrakenInFlightOrder] = {} - for key, value in saved_states.items(): - in_flight_orders[key] = KrakenInFlightOrder.from_json(value) - self._last_userref = max(int(value["userref"]), self._last_userref) - self._in_flight_orders.update(in_flight_orders) - - async def get_asset_pairs(self) -> Dict[str, Any]: - if not self._asset_pairs: - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - asset_pairs = await self._api_request(method="get", endpoint=CONSTANTS.ASSET_PAIRS_PATH_URL) - self._asset_pairs = {f"{details['base']}-{details['quote']}": details - for _, details in asset_pairs.items() if not is_dark_pool(details)} - return self._asset_pairs - - async def _get_rest_assistant(self) -> RESTAssistant: - if self._rest_assistant is None: - self._rest_assistant = await self._api_factory.get_rest_assistant() - return self._rest_assistant - - async def _update_balances(self): - cdef: - dict open_orders - dict balances - str asset_name - str balance - str base - str quote - set local_asset_names = set(self._account_balances.keys()) - set remote_asset_names = set() - set asset_names_to_remove - - balances = await self._api_request_with_retry("POST", CONSTANTS.BALANCE_PATH_URL, is_auth_required=True) - open_orders = await self._api_request_with_retry("POST", CONSTANTS.OPEN_ORDERS_PATH_URL, is_auth_required=True) - - locked = defaultdict(Decimal) - - for order in open_orders.get("open").values(): - if order.get("status") == "open": - details = order.get("descr") - if details.get("ordertype") == "limit": - pair = convert_from_exchange_trading_pair( - details.get("pair"), tuple((await self.get_asset_pairs()).keys()) - ) - (base, quote) = self.split_trading_pair(pair) - vol_locked = Decimal(order.get("vol", 0)) - Decimal(order.get("vol_exec", 0)) - if details.get("type") == "sell": - locked[convert_from_exchange_symbol(base)] += vol_locked - elif details.get("type") == "buy": - locked[convert_from_exchange_symbol(quote)] += vol_locked * Decimal(details.get("price")) - - for asset_name, balance in balances.items(): - cleaned_name = convert_from_exchange_symbol(asset_name).upper() - total_balance = Decimal(balance) - free_balance = total_balance - Decimal(locked[cleaned_name]) - self._account_available_balances[cleaned_name] = free_balance - self._account_balances[cleaned_name] = total_balance - remote_asset_names.add(cleaned_name) - - asset_names_to_remove = local_asset_names.difference(remote_asset_names) - for asset_name in asset_names_to_remove: - del self._account_available_balances[asset_name] - del self._account_balances[asset_name] - - self._in_flight_orders_snapshot = {k: copy.copy(v) for k, v in self._in_flight_orders.items()} - self._in_flight_orders_snapshot_timestamp = self._current_timestamp - - cdef object c_get_fee(self, - str base_currency, - str quote_currency, - object order_type, - object order_side, - object amount, - object price, - object is_maker = None): - """ - To get trading fee, this function is simplified by using fee override configuration. Most parameters to this - function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for - maker order. - """ - is_maker = order_type is OrderType.LIMIT_MAKER - return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker)) - - async def _update_trading_rules(self): - cdef: - # The poll interval for withdraw rules is 60 seconds. - int64_t last_tick = (self._last_timestamp / 60.0) - int64_t current_tick = (self._current_timestamp / 60.0) - if current_tick > last_tick or len(self._trading_rules) < 1: - asset_pairs = await self.get_asset_pairs() - trading_rules_list = self._format_trading_rules(asset_pairs) - self._trading_rules.clear() - for trading_rule in trading_rules_list: - self._trading_rules[convert_from_exchange_trading_pair(trading_rule.trading_pair)] = trading_rule - - def _format_trading_rules(self, asset_pairs_dict: Dict[str, Any]) -> List[TradingRule]: - """ - Example: - { - "XBTUSDT": { - "altname": "XBTUSDT", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } - } - """ - cdef: - list retval = [] - for trading_pair, rule in asset_pairs_dict.items(): - try: - base, quote = split_to_base_quote(trading_pair) - base = convert_from_exchange_symbol(base) - min_order_size = Decimal(rule.get('ordermin', 0)) - min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") - min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") - retval.append( - TradingRule( - trading_pair, - min_order_size=min_order_size, - min_price_increment=min_price_increment, - min_base_amount_increment=min_base_amount_increment, - ) - ) - except Exception: - self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True) - return retval - - async def _update_order_status(self): - cdef: - # This is intended to be a backup measure to close straggler orders, in case Kraken's user stream events - # are not working. - # The poll interval for order status is 10 seconds. - int64_t last_tick = (self._last_pull_timestamp / 10.0) - int64_t current_tick = (self._current_timestamp / 10.0) - - if len(self._in_flight_orders) > 0: - tracked_orders = list(self._in_flight_orders.values()) - tasks = [self._api_request_with_retry("POST", - CONSTANTS.QUERY_ORDERS_PATH_URL, - data={"txid": o.exchange_order_id}, - is_auth_required=True) - for o in tracked_orders] - results = await safe_gather(*tasks, return_exceptions=True) - - for order_update, tracked_order in zip(results, tracked_orders): - client_order_id = tracked_order.client_order_id - - # If the order has already been cancelled or has failed do nothing - if client_order_id not in self._in_flight_orders: - continue - - if isinstance(order_update, Exception): - self.logger().network( - f"Error fetching status update for the order {client_order_id}: {order_update}.", - app_warning_msg=f"Failed to fetch status update for the order {client_order_id}." - ) - continue - - if order_update.get("error") is not None and "EOrder:Invalid order" not in order_update["error"]: - self.logger().debug(f"Error in fetched status update for order {client_order_id}: " - f"{order_update['error']}") - self.c_cancel(tracked_order.trading_pair, tracked_order.client_order_id) - continue - - update = order_update.get(tracked_order.exchange_order_id) - - if not update: - self._order_not_found_records[client_order_id] = \ - self._order_not_found_records.get(client_order_id, 0) + 1 - if self._order_not_found_records[client_order_id] < self.ORDER_NOT_EXIST_CONFIRMATION_COUNT: - # Wait until the order not found error have repeated a few times before actually treating - # it as failed. See: https://github.com/CoinAlpha/hummingbot/issues/601 - continue - self.c_trigger_event( - self.MARKET_ORDER_FAILURE_EVENT_TAG, - MarketOrderFailureEvent(self._current_timestamp, client_order_id, tracked_order.order_type) - ) - self.c_stop_tracking_order(client_order_id) - continue - - # Update order execution status - tracked_order.last_state = update["status"] - executed_amount_base = Decimal(update["vol_exec"]) - executed_amount_quote = executed_amount_base * Decimal(update["price"]) - - if tracked_order.is_done: - if not tracked_order.is_failure: - if tracked_order.trade_type is TradeType.BUY: - self.logger().info(f"The market buy order {tracked_order.client_order_id} has completed " - f"according to order status API.") - self.c_trigger_event(self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG, - BuyOrderCompletedEvent(self._current_timestamp, - client_order_id, - tracked_order.base_asset, - tracked_order.quote_asset, - executed_amount_base, - executed_amount_quote, - tracked_order.order_type)) - else: - self.logger().info(f"The market sell order {client_order_id} has completed " - f"according to order status API.") - self.c_trigger_event(self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG, - SellOrderCompletedEvent(self._current_timestamp, - client_order_id, - tracked_order.base_asset, - tracked_order.quote_asset, - executed_amount_base, - executed_amount_quote, - tracked_order.order_type)) - else: - # check if its a cancelled order - # if its a cancelled order, issue cancel and stop tracking order - if tracked_order.is_cancelled: - self.logger().info(f"Successfully canceled order {client_order_id}.") - self.c_trigger_event(self.MARKET_ORDER_CANCELED_EVENT_TAG, - OrderCancelledEvent( - self._current_timestamp, - client_order_id)) - else: - self.logger().info(f"The market order {client_order_id} has failed according to " - f"order status API.") - self.c_trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, - MarketOrderFailureEvent( - self._current_timestamp, - client_order_id, - tracked_order.order_type - )) - self.c_stop_tracking_order(client_order_id) - - async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, Any]]: - while True: - try: - yield await self._user_stream_tracker.user_stream.get() - except asyncio.CancelledError: - raise - except Exception: - self.logger().network( - "Unknown error. Retrying after 1 seconds.", - exc_info=True, - app_warning_msg="Could not fetch user events from Kraken. Check API key and network connection." - ) - await asyncio.sleep(1.0) - - async def _user_stream_event_listener(self): - async for event_message in self._iter_user_event_queue(): - try: - # Event type is second from last, there is newly added sequence number (last item). - # https://docs.kraken.com/websockets/#sequence-numbers - event_type: str = event_message[-2] - updates: List[Any] = event_message[0] - if event_type == "ownTrades": - for update in updates: - trade_id: str = next(iter(update)) - trade: Dict[str, str] = update[trade_id] - trade["trade_id"] = trade_id - exchange_order_id = trade.get("ordertxid") - try: - client_order_id = next(key for key, value in self._in_flight_orders.items() - if value.exchange_order_id == exchange_order_id) - except StopIteration: - continue - - tracked_order = self._in_flight_orders.get(client_order_id) - - if tracked_order is None: - # Hiding the messages for now. Root cause to be investigated in later sprints. - self.logger().debug(f"Unrecognized order ID from user stream: {client_order_id}.") - self.logger().debug(f"Event: {event_message}") - self.logger().debug(f"Order Event: {update}") - continue - - updated: bool = tracked_order.update_with_trade_update(trade) - if updated: - self.c_trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, - OrderFilledEvent(self._current_timestamp, - tracked_order.client_order_id, - tracked_order.trading_pair, - tracked_order.trade_type, - tracked_order.order_type, - Decimal(trade.get("price")), - Decimal(trade.get("vol")), - AddedToCostTradeFee( - flat_fees=[ - TokenAmount( - tracked_order.fee_asset, - Decimal((trade.get("fee"))), - ) - ] - ), - trade.get("trade_id"))) - - if tracked_order.is_done: - if not tracked_order.is_failure: - if tracked_order.trade_type is TradeType.BUY: - self.logger().info(f"The market buy order {tracked_order.client_order_id} has completed " - f"according to user stream.") - self.c_trigger_event(self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG, - BuyOrderCompletedEvent(self._current_timestamp, - tracked_order.client_order_id, - tracked_order.base_asset, - tracked_order.quote_asset, - tracked_order.executed_amount_base, - tracked_order.executed_amount_quote, - tracked_order.order_type)) - else: - self.logger().info(f"The market sell order {tracked_order.client_order_id} has completed " - f"according to user stream.") - self.c_trigger_event(self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG, - SellOrderCompletedEvent(self._current_timestamp, - tracked_order.client_order_id, - tracked_order.base_asset, - tracked_order.quote_asset, - tracked_order.executed_amount_base, - tracked_order.executed_amount_quote, - tracked_order.order_type)) - else: - # check if its a cancelled order - # if its a cancelled order, check in flight orders - # if present in in flight orders issue cancel and stop tracking order - if tracked_order.is_cancelled: - if tracked_order.client_order_id in self._in_flight_orders: - self.logger().info(f"Successfully canceled order {tracked_order.client_order_id}.") - self.c_trigger_event(self.MARKET_ORDER_CANCELED_EVENT_TAG, - OrderCancelledEvent(self._current_timestamp, - tracked_order.client_order_id)) - else: - self.logger().info(f"The market order {tracked_order.client_order_id} has failed according to " - f"order status API.") - self.c_trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, - MarketOrderFailureEvent(self._current_timestamp, - tracked_order.client_order_id, - tracked_order.order_type)) - - self.c_stop_tracking_order(tracked_order.client_order_id) - - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unexpected error in user stream listener loop.", exc_info=True) - await asyncio.sleep(5.0) - - async def _status_polling_loop(self): - while True: - try: - self._poll_notifier = asyncio.Event() - await self._poll_notifier.wait() - await safe_gather( - self._update_balances(), - self._update_order_status(), - ) - self._last_pull_timestamp = self._current_timestamp - except asyncio.CancelledError: - raise - except Exception: - self.logger().network("Unexpected error while fetching account updates.", exc_info=True, - app_warning_msg="Could not fetch account updates from Kraken. " - "Check API key and network connection.") - await asyncio.sleep(0.5) - - async def _trading_rules_polling_loop(self): - while True: - try: - await safe_gather( - self._update_trading_rules(), - ) - await asyncio.sleep(60) - except asyncio.CancelledError: - raise - except Exception: - self.logger().network("Unexpected error while fetching trading rules.", exc_info=True, - app_warning_msg="Could not fetch new trading rules from Kraken. " - "Check network connection.") - await asyncio.sleep(0.5) - - @property - def status_dict(self) -> Dict[str, bool]: - return { - "order_books_initialized": self.order_book_tracker.ready, - "account_balance": len(self._account_balances) > 0 if self._trading_required else True, - "trading_rule_initialized": len(self._trading_rules) > 0, - } - - @property - def ready(self) -> bool: - return all(self.status_dict.values()) - - cdef c_start(self, Clock clock, double timestamp): - self._tx_tracker.c_start(clock, timestamp) - ExchangeBase.c_start(self, clock, timestamp) - - cdef c_stop(self, Clock clock): - ExchangeBase.c_stop(self, clock) - self._async_scheduler.stop() - - async def start_network(self): - self._stop_network() - self.order_book_tracker.start() - self._trading_rules_polling_task = safe_ensure_future(self._trading_rules_polling_loop()) - if self._trading_required: - self._status_polling_task = safe_ensure_future(self._status_polling_loop()) - self._user_stream_tracker_task = safe_ensure_future(self._user_stream_tracker.start()) - self._user_stream_event_listener_task = safe_ensure_future(self._user_stream_event_listener()) - - def _stop_network(self): - self.order_book_tracker.stop() - if self._status_polling_task is not None: - self._status_polling_task.cancel() - if self._user_stream_tracker_task is not None: - self._user_stream_tracker_task.cancel() - if self._user_stream_event_listener_task is not None: - self._user_stream_event_listener_task.cancel() - if self._trading_rules_polling_task is not None: - self._trading_rules_polling_task.cancel() - self._status_polling_task = self._user_stream_tracker_task = \ - self._user_stream_event_listener_task = None - - async def stop_network(self): - self._stop_network() - - async def check_network(self) -> NetworkStatus: - try: - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - request = RESTRequest( - method=RESTMethod.GET, - url=url - ) - rest_assistant = await self._get_rest_assistant() - async with self._throttler.execute_task(CONSTANTS.TIME_PATH_URL): - resp = await rest_assistant.call(request=request) - if resp.status != 200: - raise ConnectionError - except asyncio.CancelledError: - raise - except Exception: - return NetworkStatus.NOT_CONNECTED - return NetworkStatus.CONNECTED - - cdef c_tick(self, double timestamp): - cdef: - int64_t last_tick = (self._last_timestamp / self._poll_interval) - int64_t current_tick = (timestamp / self._poll_interval) - ExchangeBase.c_tick(self, timestamp) - self._tx_tracker.c_tick(timestamp) - if current_tick > last_tick: - if not self._poll_notifier.is_set(): - self._poll_notifier.set() - self._last_timestamp = timestamp - - def generate_userref(self): - self._last_userref += 1 - return self._last_userref - - @staticmethod - def is_cloudflare_exception(exception: Exception): - """ - Error status 5xx or 10xx are related to Cloudflare. - https://support.kraken.com/hc/en-us/articles/360001491786-API-error-messages#6 - """ - return bool(re.search(r"HTTP status is (5|10)\d\d\.", str(exception))) - - async def get_open_orders_with_userref(self, userref: int): - data = {'userref': userref} - return await self._api_request_with_retry("POST", - CONSTANTS.OPEN_ORDERS_PATH_URL, - is_auth_required=True, - data=data) - - async def _api_request_with_retry(self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - is_auth_required: bool = False, - retry_interval = 2.0) -> Dict[str, Any]: - result = None - for retry_attempt in range(self.REQUEST_ATTEMPTS): - try: - result= await self._api_request(method, endpoint, params, data, is_auth_required) - break - except IOError as e: - if self.is_cloudflare_exception(e): - if endpoint == CONSTANTS.ADD_ORDER_PATH_URL: - self.logger().info(f"Retrying {endpoint}") - # Order placement could have been successful despite the IOError, so check for the open order. - response = self.get_open_orders_with_userref(data.get('userref')) - if any(response.get("open").values()): - return response - self.logger().warning( - f"Cloudflare error. Attempt {retry_attempt+1}/{self.REQUEST_ATTEMPTS}" - f" API command {method}: {endpoint}" - ) - await asyncio.sleep(retry_interval ** retry_attempt) - continue - else: - raise e - if result is None: - raise IOError(f"Error fetching data from {endpoint}.") - return result - - async def _api_request(self, - method: str, - endpoint: str, - params: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - is_auth_required: bool = False) -> Dict[str, Any]: - async with self._throttler.execute_task(endpoint): - url = f"{CONSTANTS.BASE_URL}{endpoint}" - headers = {} - data_dict = data if data is not None else {} - - if is_auth_required: - auth_dict: Dict[str, Any] = self._kraken_auth.generate_auth_dict(endpoint, data=data) - headers.update(auth_dict["headers"]) - data_dict = auth_dict["postDict"] - - request = RESTRequest( - method=RESTMethod[method.upper()], - url=url, - headers=headers, - params=params, - data=data_dict - ) - rest_assistant = await self._get_rest_assistant() - response = await rest_assistant.call(request=request, timeout=100) - - if response.status != 200: - raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}.") - try: - response_json = await response.json() - except Exception: - raise IOError(f"Error parsing data from {url}.") - - try: - err = response_json["error"] - if "EOrder:Unknown order" in err or "EOrder:Invalid order" in err: - return {"error": err} - elif "EAPI:Invalid nonce" in err: - self.logger().error(f"Invalid nonce error from {url}. " + - "Please ensure your Kraken API key nonce window is at least 10, " + - "and if needed reset your API key.") - raise IOError({"error": response_json}) - except IOError: - raise - except Exception: - pass - - data = response_json.get("result") - if data is None: - self.logger().error(f"Error received from {url}. Response is {response_json}.") - raise IOError({"error": response_json}) - return data - - async def get_order(self, client_order_id: str) -> Dict[str, Any]: - o = self._in_flight_orders.get(client_order_id) - result = await self._api_request_with_retry("POST", - CONSTANTS.QUERY_ORDERS_PATH_URL, - data={"txid": o.exchange_order_id}, - is_auth_required=True) - return result - - def supported_order_types(self): - return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] - - async def place_order(self, - userref: int, - trading_pair: str, - amount: Decimal, - order_type: OrderType, - is_buy: bool, - price: Optional[Decimal] = s_decimal_NaN): - - trading_pair = convert_to_exchange_trading_pair(trading_pair) - data = { - "pair": trading_pair, - "type": "buy" if is_buy else "sell", - "ordertype": "market" if order_type is OrderType.MARKET else "limit", - "volume": str(amount), - "userref": userref, - "price": str(price) - } - if order_type is OrderType.LIMIT_MAKER: - data["oflags"] = "post" - return await self._api_request_with_retry("post", - CONSTANTS.ADD_ORDER_PATH_URL, - data=data, - is_auth_required=True) - - async def execute_buy(self, - order_id: str, - trading_pair: str, - amount: Decimal, - order_type: OrderType, - price: Optional[Decimal] = s_decimal_NaN, - userref: int = 0): - cdef: - TradingRule trading_rule = self._trading_rules[trading_pair] - str base_currency = self.split_trading_pair(trading_pair)[0] - str quote_currency = self.split_trading_pair(trading_pair)[1] - - decimal_amount = self.c_quantize_order_amount(trading_pair, amount) - decimal_price = self.c_quantize_order_price(trading_pair, price) - if decimal_amount < trading_rule.min_order_size: - raise ValueError(f"Buy order amount {decimal_amount} is lower than the minimum order size " - f"{trading_rule.min_order_size}.") - - try: - order_result = None - order_decimal_amount = f"{decimal_amount:f}" - if order_type in self.supported_order_types(): - order_decimal_price = f"{decimal_price:f}" - self.c_start_tracking_order( - order_id, - "", - trading_pair, - TradeType.BUY, - decimal_price, - decimal_amount, - order_type, - userref - ) - order_result = await self.place_order(userref=userref, - trading_pair=trading_pair, - amount=order_decimal_amount, - order_type=order_type, - is_buy=True, - price=order_decimal_price) - else: - raise ValueError(f"Invalid OrderType {order_type}. Aborting.") - - exchange_order_id = order_result["txid"][0] - tracked_order = self._in_flight_orders.get(order_id) - if tracked_order is not None: - self.logger().info(f"Created {order_type} buy order {order_id} for " - f"{decimal_amount} {trading_pair}.") - tracked_order.update_exchange_order_id(exchange_order_id) - self.c_trigger_event(self.MARKET_BUY_ORDER_CREATED_EVENT_TAG, - BuyOrderCreatedEvent( - self._current_timestamp, - order_type, - trading_pair, - decimal_amount, - decimal_price, - order_id, - tracked_order.creation_timestamp - )) - - except asyncio.CancelledError: - raise - - except Exception as e: - self.c_stop_tracking_order(order_id) - if order_type is OrderType.LIMIT: - order_type_str = 'LIMIT' - elif order_type is OrderType.LIMIT_MAKER: - order_type_str = 'LIMIT_MAKER' - else: - order_type_str = 'MARKET' - self.logger().network( - f"Error submitting buy {order_type_str} order to Kraken for " - f"{decimal_amount} {trading_pair}" - f" {decimal_price}.", - exc_info=True, - app_warning_msg=f"Failed to submit buy order to Kraken. Check API key and network connection." - ) - self.c_trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, - MarketOrderFailureEvent(self._current_timestamp, order_id, order_type)) - - cdef str c_buy(self, str trading_pair, object amount, object order_type=OrderType.LIMIT, object price=s_decimal_NaN, - dict kwargs={}): - cdef: - int64_t tracking_nonce = get_tracking_nonce() - int32_t userref = self.generate_userref() - str order_id = str(f"buy-{trading_pair}-{tracking_nonce}") - safe_ensure_future(self.execute_buy(order_id, trading_pair, amount, order_type, price=price, userref=userref)) - return order_id - - async def execute_sell(self, - order_id: str, - trading_pair: str, - amount: Decimal, - order_type: OrderType, - price: Optional[Decimal] = Decimal("NaN"), - userref: int = 0): - cdef: - TradingRule trading_rule = self._trading_rules[trading_pair] - - decimal_amount = self.quantize_order_amount(trading_pair, amount) - decimal_price = self.c_quantize_order_price(trading_pair, price) - - if decimal_amount < trading_rule.min_order_size: - raise ValueError(f"Sell order amount {decimal_amount} is lower than the minimum order size " - f"{trading_rule.min_order_size}.") - - try: - order_result = None - order_decimal_amount = f"{decimal_amount:f}" - if order_type in self.supported_order_types(): - order_decimal_price = f"{decimal_price:f}" - self.c_start_tracking_order( - order_id, - "", - trading_pair, - TradeType.SELL, - decimal_price, - decimal_amount, - order_type, - userref - ) - order_result = await self.place_order(userref=userref, - trading_pair=trading_pair, - amount=order_decimal_amount, - order_type=order_type, - is_buy=False, - price=order_decimal_price) - else: - raise ValueError(f"Invalid OrderType {order_type}. Aborting.") - - exchange_order_id = order_result["txid"][0] - tracked_order = self._in_flight_orders.get(order_id) - if tracked_order is not None: - self.logger().info(f"Created {order_type} sell order {order_id} for " - f"{decimal_amount} {trading_pair}.") - tracked_order.update_exchange_order_id(exchange_order_id) - self.c_trigger_event(self.MARKET_SELL_ORDER_CREATED_EVENT_TAG, - SellOrderCreatedEvent( - self._current_timestamp, - order_type, - trading_pair, - decimal_amount, - decimal_price, - order_id, - tracked_order.creation_timestamp, - )) - except asyncio.CancelledError: - raise - except Exception: - self.c_stop_tracking_order(order_id) - if order_type is OrderType.LIMIT: - order_type_str = 'LIMIT' - elif order_type is OrderType.LIMIT_MAKER: - order_type_str = 'LIMIT_MAKER' - else: - order_type_str = 'MAKER' - self.logger().network( - f"Error submitting sell {order_type_str} order to Kraken for " - f"{decimal_amount} {trading_pair} " - f"{decimal_price}.", - exc_info=True, - app_warning_msg=f"Failed to submit sell order to Kraken. Check API key and network connection." - ) - self.c_trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, - MarketOrderFailureEvent(self._current_timestamp, order_id, order_type)) - - cdef str c_sell(self, - str trading_pair, - object amount, - object order_type=OrderType.LIMIT, - object price=s_decimal_NaN, - dict kwargs={}): - cdef: - int64_t tracking_nonce = get_tracking_nonce() - int32_t userref = self.generate_userref() - str order_id = str(f"sell-{trading_pair}-{tracking_nonce}") - safe_ensure_future(self.execute_sell(order_id, trading_pair, amount, order_type, price=price, userref=userref)) - return order_id - - async def execute_cancel(self, trading_pair: str, order_id: str): - try: - tracked_order = self._in_flight_orders.get(order_id) - if tracked_order is None: - raise ValueError(f"Failed to cancel order – {order_id}. Order not found.") - if tracked_order.is_local: - raise KrakenInFlightOrderNotCreated(f"Failed to cancel order - {order_id}. Order not yet created.") - - data: Dict[str, str] = {"txid": tracked_order.exchange_order_id} - cancel_result = await self._api_request_with_retry("POST", - CONSTANTS.CANCEL_ORDER_PATH_URL, - data=data, - is_auth_required=True) - - if isinstance(cancel_result, dict) and (cancel_result.get("count") == 1 or cancel_result.get("error") is not None): - self.logger().info(f"Successfully canceled order {order_id}.") - self.c_stop_tracking_order(order_id) - self.c_trigger_event(self.MARKET_ORDER_CANCELED_EVENT_TAG, - OrderCancelledEvent(self._current_timestamp, order_id)) - return { - "origClientOrderId": order_id - } - except KrakenInFlightOrderNotCreated: - raise - except Exception as e: - self.logger().warning(f"Error canceling order on Kraken", - exc_info=True) - - cdef c_cancel(self, str trading_pair, str order_id): - safe_ensure_future(self.execute_cancel(trading_pair, order_id)) - return order_id - - async def cancel_all(self, timeout_seconds: float) -> List[CancellationResult]: - incomplete_orders = [(key, o) for (key, o) in self._in_flight_orders.items() if not o.is_done] - tasks = [self.execute_cancel(o.trading_pair, key) for (key, o) in incomplete_orders] - order_id_set = set([key for (key, o) in incomplete_orders]) - successful_cancellations = [] - - try: - async with timeout(timeout_seconds): - cancellation_results = await safe_gather(*tasks, return_exceptions=True) - for cr in cancellation_results: - if isinstance(cr, Exception): - continue - if isinstance(cr, dict) and "origClientOrderId" in cr: - client_order_id = cr.get("origClientOrderId") - order_id_set.remove(client_order_id) - successful_cancellations.append(CancellationResult(client_order_id, True)) - except Exception: - self.logger().network( - f"Unexpected error canceling orders.", - exc_info=True, - app_warning_msg="Failed to cancel order with Kraken. Check API key and network connection." - ) - - failed_cancellations = [CancellationResult(oid, False) for oid in order_id_set] - return successful_cancellations + failed_cancellations - - cdef OrderBook c_get_order_book(self, str trading_pair): - cdef: - dict order_books = self.order_book_tracker.order_books - - if trading_pair not in order_books: - raise ValueError(f"No order book exists for '{trading_pair}'.") - return order_books[trading_pair] - - cdef c_did_timeout_tx(self, str tracking_id): - self.c_trigger_event(self.MARKET_TRANSACTION_FAILURE_EVENT_TAG, - MarketTransactionFailureEvent(self._current_timestamp, tracking_id)) - - def start_tracking_order(self, - order_id: str, - exchange_order_id: str, - trading_pair: str, - trade_type: TradeType, - price: float, - amount: float, - order_type: OrderType, - userref: int): - """Used for testing.""" - self.c_start_tracking_order( - order_id, exchange_order_id, trading_pair, trade_type, price, amount, order_type, userref - ) - - cdef c_start_tracking_order(self, - str order_id, - str exchange_order_id, - str trading_pair, - object trade_type, - object price, - object amount, - object order_type, - int userref): - self._in_flight_orders[order_id] = KrakenInFlightOrder( - client_order_id=order_id, - exchange_order_id=exchange_order_id, - trading_pair=trading_pair, - trade_type=trade_type, - price=price, - amount=amount, - order_type=order_type, - creation_timestamp=self.current_timestamp, - userref=userref - ) - - cdef c_stop_tracking_order(self, str order_id): - if order_id in self._in_flight_orders: - del self._in_flight_orders[order_id] - if order_id in self._order_not_found_records: - del self._order_not_found_records[order_id] - - cdef object c_get_order_price_quantum(self, str trading_pair, object price): - cdef: - TradingRule trading_rule = self._trading_rules[trading_pair] - return trading_rule.min_price_increment - - cdef object c_get_order_size_quantum(self, str trading_pair, object order_size): - cdef: - TradingRule trading_rule = self._trading_rules[trading_pair] - return Decimal(trading_rule.min_base_amount_increment) - - cdef object c_quantize_order_amount(self, str trading_pair, object amount, object price=s_decimal_0): - cdef: - TradingRule trading_rule = self._trading_rules[trading_pair] - object quantized_amount = ExchangeBase.c_quantize_order_amount(self, trading_pair, amount) - - global s_decimal_0 - if quantized_amount < trading_rule.min_order_size: - return s_decimal_0 - - return quantized_amount - - def get_price(self, trading_pair: str, is_buy: bool) -> Decimal: - return self.c_get_price(trading_pair, is_buy) - - def buy(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET, - price: Decimal = s_decimal_NaN, **kwargs) -> str: - return self.c_buy(trading_pair, amount, order_type, price, kwargs) - - def sell(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET, - price: Decimal = s_decimal_NaN, **kwargs) -> str: - return self.c_sell(trading_pair, amount, order_type, price, kwargs) - - def cancel(self, trading_pair: str, client_order_id: str): - return self.c_cancel(trading_pair, client_order_id) - - def get_fee(self, - base_currency: str, - quote_currency: str, - order_type: OrderType, - order_side: TradeType, - amount: Decimal, - price: Decimal = s_decimal_NaN, - is_maker: Optional[bool] = None) -> AddedToCostTradeFee: - return self.c_get_fee(base_currency, quote_currency, order_type, order_side, amount, price, is_maker) - - def get_order_book(self, trading_pair: str) -> OrderBook: - return self.c_get_order_book(trading_pair) - - def _build_async_throttler(self, api_tier: KrakenAPITier) -> AsyncThrottler: - limits_pct = self._client_config.rate_limits_share_pct - if limits_pct < Decimal("100"): - self.logger().warning( - f"The Kraken API does not allow enough bandwidth for a reduced rate-limit share percentage." - f" Current percentage: {limits_pct}." - ) - throttler = AsyncThrottler(build_rate_limits_by_tier(api_tier)) - return throttler - - async def all_trading_pairs(self) -> List[str]: - # This method should be removed and instead we should implement _initialize_trading_pair_symbol_map - return await KrakenAPIOrderBookDataSource.fetch_trading_pairs(throttler=self._throttler) - - async def get_last_traded_prices(self, trading_pairs: List[str]) -> Dict[str, float]: - # This method should be removed and instead we should implement _get_last_traded_price - return await KrakenAPIOrderBookDataSource.get_last_traded_prices( - trading_pairs=trading_pairs, - throttler=self._throttler) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd deleted file mode 100644 index 7cf591a66e..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pxd +++ /dev/null @@ -1,6 +0,0 @@ -from hummingbot.connector.in_flight_order_base cimport InFlightOrderBase - -cdef class KrakenInFlightOrder(InFlightOrderBase): - cdef: - public object trade_id_set - public int userref diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx deleted file mode 100644 index 2e2362557b..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_in_flight_order.pyx +++ /dev/null @@ -1,101 +0,0 @@ -import math -from decimal import Decimal -from typing import ( - Any, - Dict, - List, -) - -from hummingbot.connector.in_flight_order_base import InFlightOrderBase -from hummingbot.core.data_type.common import OrderType, TradeType - -s_decimal_0 = Decimal(0) - - -class KrakenInFlightOrderNotCreated(Exception): - pass - - -cdef class KrakenInFlightOrder(InFlightOrderBase): - def __init__(self, - client_order_id: str, - exchange_order_id: str, - trading_pair: str, - order_type: OrderType, - trade_type: TradeType, - price: Decimal, - amount: Decimal, - creation_timestamp: float, - userref: int, - initial_state: str = "local"): - super().__init__( - client_order_id, - exchange_order_id, - trading_pair, - order_type, - trade_type, - price, - amount, - creation_timestamp, - initial_state, - ) - self.trade_id_set = set() - self.userref = userref - - # todo - @property - def is_local(self) -> bool: - return self.last_state in {"local"} - - @property - def is_done(self) -> bool: - return self.last_state in {"closed", "canceled", "expired"} - - @property - def is_failure(self) -> bool: - return self.last_state in {"canceled", "expired"} - - @property - def is_cancelled(self) -> bool: - return self.last_state in {"canceled"} - - @classmethod - def _instance_creation_parameters_from_json(cls, data: Dict[str, Any]) -> List[Any]: - arguments: List[Any] = super()._instance_creation_parameters_from_json(data) - arguments.insert(-1, data["userref"]) - return arguments - - def to_json(self): - json = super().to_json() - json.update({"userref": self.userref}) - return json - - def update_exchange_order_id(self, exchange_id: str): - super().update_exchange_order_id(exchange_id) - self.last_state = "new" - - def _mark_as_filled(self): - """ - Updates the status of the InFlightOrder as filled. - Note: Should only be called when order is completely filled. - """ - self.last_state = "closed" - - def update_with_trade_update(self, trade_update: Dict[str, Any]) -> bool: - """ - Updartes the InFlightOrder with the trade update (from WebSocket API ownTrades stream) - :return: True if the order gets updated otherwise False - """ - trade_id = trade_update["trade_id"] - if str(trade_update["ordertxid"]) != self.exchange_order_id or trade_id in self.trade_id_set: - # trade already recorded - return False - self.trade_id_set.add(trade_id) - self.executed_amount_base += Decimal(trade_update["vol"]) - self.fee_paid += Decimal(trade_update["fee"]) - self.executed_amount_quote += Decimal(trade_update["vol"]) * Decimal(trade_update["price"]) - if not self.fee_asset: - self.fee_asset = self.quote_asset - if (math.isclose(self.executed_amount_base, self.amount) or self.executed_amount_base >= self.amount): - self._mark_as_filled() - return True diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd b/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd deleted file mode 100644 index 69517f8165..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pxd +++ /dev/null @@ -1,4 +0,0 @@ -from hummingbot.core.data_type.order_book cimport OrderBook - -cdef class KrakenOrderBook(OrderBook): - pass diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx b/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx deleted file mode 100644 index 08b080302b..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_order_book.pyx +++ /dev/null @@ -1,86 +0,0 @@ -import logging -from typing import ( - Dict, - Optional -) - -from hummingbot.core.data_type.common import TradeType -from hummingbot.core.data_type.order_book cimport OrderBook -from hummingbot.core.data_type.order_book_message import ( - OrderBookMessage, - OrderBookMessageType -) -from hummingbot.logger import HummingbotLogger - -_krob_logger = None - - -cdef class KrakenOrderBook(OrderBook): - @classmethod - def logger(cls) -> HummingbotLogger: - global _krob_logger - if _krob_logger is None: - _krob_logger = logging.getLogger(__name__) - return _krob_logger - - @classmethod - def snapshot_message_from_exchange(cls, - msg: Dict[str, any], - timestamp: float, - metadata: Optional[Dict] = None) -> OrderBookMessage: - if metadata: - msg.update(metadata) - return OrderBookMessage(OrderBookMessageType.SNAPSHOT, { - "trading_pair": msg["trading_pair"].replace("/", ""), - "update_id": msg["latest_update"], - "bids": msg["bids"], - "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) - - @classmethod - def diff_message_from_exchange(cls, - msg: Dict[str, any], - timestamp: Optional[float] = None, - metadata: Optional[Dict] = None) -> OrderBookMessage: - if metadata: - msg.update(metadata) - return OrderBookMessage(OrderBookMessageType.DIFF, { - "trading_pair": msg["trading_pair"].replace("/", ""), - "update_id": msg["update_id"], - "bids": msg["bids"], - "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) - - @classmethod - def snapshot_ws_message_from_exchange(cls, - msg: Dict[str, any], - timestamp: Optional[float] = None, - metadata: Optional[Dict] = None) -> OrderBookMessage: - if metadata: - msg.update(metadata) - return OrderBookMessage(OrderBookMessageType.SNAPSHOT, { - "trading_pair": msg["trading_pair"].replace("/", ""), - "update_id": msg["update_id"], - "bids": msg["bids"], - "asks": msg["asks"] - }, timestamp=timestamp * 1e-3) - - @classmethod - def trade_message_from_exchange(cls, msg: Dict[str, any], metadata: Optional[Dict] = None): - if metadata: - msg.update(metadata) - ts = float(msg["trade"][2]) - return OrderBookMessage(OrderBookMessageType.TRADE, { - "trading_pair": msg["pair"].replace("/", ""), - "trade_type": float(TradeType.SELL.value) if msg["trade"][3] == "s" else float(TradeType.BUY.value), - "trade_id": ts, - "update_id": ts, - "price": msg["trade"][0], - "amount": msg["trade"][1] - }, timestamp=ts * 1e-3) - - @classmethod - def from_snapshot(cls, msg: OrderBookMessage) -> "OrderBook": - retval = KrakenOrderBook() - retval.apply_snapshot(msg.bids, msg.asks, msg.update_id) - return retval diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py b/hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py deleted file mode 100644 index d41c2139d4..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_order_book_tracker.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python - -import asyncio -from collections import deque, defaultdict -import logging -import time -from typing import ( - Deque, - Dict, - List, - Optional -) - -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.logger import HummingbotLogger -from hummingbot.core.data_type.order_book_tracker import ( - OrderBookTracker -) -from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource -from hummingbot.core.data_type.order_book import OrderBook -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.utils.async_utils import wait_til -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory - - -class KrakenOrderBookTracker(OrderBookTracker): - _krobt_logger: Optional[HummingbotLogger] = None - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._krobt_logger is None: - cls._krobt_logger = logging.getLogger(__name__) - return cls._krobt_logger - - def __init__(self, - trading_pairs: List[str], - throttler: Optional[AsyncThrottler] = None, - api_factory: Optional[WebAssistantsFactory] = None, - ): - super().__init__(KrakenAPIOrderBookDataSource(throttler, trading_pairs), trading_pairs, api_factory) - self._api_factory = api_factory - self._order_book_diff_stream: asyncio.Queue = asyncio.Queue() - self._order_book_snapshot_stream: asyncio.Queue = asyncio.Queue() - self._ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() - self._saved_message_queues: Dict[str, Deque[OrderBookMessage]] = defaultdict(lambda: deque(maxlen=1000)) - - @property - def data_source(self) -> OrderBookTrackerDataSource: - if not self._data_source: - self._data_source = KrakenAPIOrderBookDataSource(trading_pairs=self._trading_pairs, - api_factory=self._api_factory) - return self._data_source - - @property - def exchange_name(self) -> str: - return "kraken" - - async def _order_book_diff_router(self): - """ - Route the real-time order book diff messages to the correct order book. - """ - last_message_timestamp: float = time.time() - messages_accepted: int = 0 - messages_rejected: int = 0 - - await wait_til(lambda: len(self.data_source._trading_pairs) == len(self._order_books.keys())) - while True: - try: - ob_message: OrderBookMessage = await self._order_book_diff_stream.get() - trading_pair: str = ob_message.trading_pair - - if trading_pair not in self._tracking_message_queues: - messages_rejected += 1 - continue - message_queue: asyncio.Queue = self._tracking_message_queues[trading_pair] - # Check the order book's initial update ID. If it's larger, don't bother. - order_book: OrderBook = self._order_books[trading_pair] - - if order_book.snapshot_uid > ob_message.update_id: - messages_rejected += 1 - continue - await message_queue.put(ob_message) - messages_accepted += 1 - - # Log some statistics. - now: float = time.time() - if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug(f"Diff messages processed: {messages_accepted}, rejected: {messages_rejected}") - messages_accepted = 0 - messages_rejected = 0 - - last_message_timestamp = now - except asyncio.CancelledError: - raise - except Exception: - self.logger().error("Unknown error. Retrying after 5 seconds.", exc_info=True) - await asyncio.sleep(5.0) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py b/hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py deleted file mode 100644 index 78f0069c64..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_tracking_nonce.py +++ /dev/null @@ -1,10 +0,0 @@ -import time - -_last_tracking_nonce: int = 0 - - -def get_tracking_nonce() -> str: - global _last_tracking_nonce - nonce = int(time.time()) - _last_tracking_nonce = nonce if nonce > _last_tracking_nonce else _last_tracking_nonce + 1 - return str(_last_tracking_nonce) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py b/hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py deleted file mode 100644 index 4660d7c02e..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_user_stream_tracker.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from typing import Optional - -from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.data_type.user_stream_tracker import UserStreamTracker -from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource -from hummingbot.core.utils.async_utils import ( - safe_ensure_future, - safe_gather, -) -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory -from hummingbot.logger import HummingbotLogger - - -class KrakenUserStreamTracker(UserStreamTracker): - _krust_logger: Optional[HummingbotLogger] = None - - @classmethod - def logger(cls) -> HummingbotLogger: - if cls._krust_logger is None: - cls._krust_logger = logging.getLogger(__name__) - return cls._krust_logger - - def __init__(self, - throttler: AsyncThrottler, - kraken_auth: KrakenAuth, - api_factory: Optional[WebAssistantsFactory] = None): - self._throttler = throttler - self._api_factory = api_factory - self._kraken_auth: KrakenAuth = kraken_auth - super().__init__(data_source=KrakenAPIUserStreamDataSource( - self._throttler, - self._kraken_auth, - self._api_factory)) - - @property - def data_source(self) -> UserStreamTrackerDataSource: - if not self._data_source: - self._data_source = KrakenAPIUserStreamDataSource(self._throttler, self._kraken_auth, self._api_factory) - return self._data_source - - @property - def exchange_name(self) -> str: - return "kraken" - - async def start(self): - self._user_stream_tracking_task = safe_ensure_future( - self.data_source.listen_for_user_stream(self._user_stream) - ) - await safe_gather(self._user_stream_tracking_task) diff --git a/hummingbot/connector/exchange/kraken_v1/kraken_utils.py b/hummingbot/connector/exchange/kraken_v1/kraken_utils.py deleted file mode 100644 index d00c27842b..0000000000 --- a/hummingbot/connector/exchange/kraken_v1/kraken_utils.py +++ /dev/null @@ -1,217 +0,0 @@ -from typing import Any, Dict, List, Optional, Tuple - -from pydantic import Field, SecretStr - -import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS -from hummingbot.client.config.config_data_types import BaseConnectorConfigMap, ClientFieldData -from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit -from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory - -CENTRALIZED = True - -EXAMPLE_PAIR = "ETH-USDC" - -DEFAULT_FEES = [0.16, 0.26] - - -def split_trading_pair(trading_pair: str) -> Tuple[str, str]: - return tuple(convert_from_exchange_trading_pair(trading_pair).split("-")) - - -def convert_from_exchange_symbol(symbol: str) -> str: - # Assuming if starts with Z or X and has 4 letters then Z/X is removable - if (symbol[0] == "X" or symbol[0] == "Z") and len(symbol) == 4: - symbol = symbol[1:] - return CONSTANTS.KRAKEN_TO_HB_MAP.get(symbol, symbol) - - -def convert_to_exchange_symbol(symbol: str) -> str: - inverted_kraken_to_hb_map = {v: k for k, v in CONSTANTS.KRAKEN_TO_HB_MAP.items()} - return inverted_kraken_to_hb_map.get(symbol, symbol) - - -def split_to_base_quote(exchange_trading_pair: str) -> Tuple[Optional[str], Optional[str]]: - base, quote = exchange_trading_pair.split("-") - return base, quote - - -def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_trading_pairs: Optional[Tuple] = None) -> Optional[str]: - base, quote = "", "" - if "-" in exchange_trading_pair: - base, quote = split_to_base_quote(exchange_trading_pair) - elif "/" in exchange_trading_pair: - base, quote = exchange_trading_pair.split("/") - elif len(available_trading_pairs) > 0: - # If trading pair has no spaces (i.e. ETHUSDT). Then it will have to match with the existing pairs - # Option 1: Using traditional naming convention - connector_trading_pair = {''.join(convert_from_exchange_trading_pair(tp).split('-')): tp for tp in available_trading_pairs}.get( - exchange_trading_pair) - if not connector_trading_pair: - # Option 2: Using kraken naming convention ( XXBT for Bitcoin, XXDG for Doge, ZUSD for USD, etc) - connector_trading_pair = {''.join(tp.split('-')): tp for tp in available_trading_pairs}.get( - exchange_trading_pair) - if not connector_trading_pair: - # Option 3: Kraken naming convention but without the initial X and Z - connector_trading_pair = {''.join(convert_to_exchange_symbol(convert_from_exchange_symbol(s)) - for s in tp.split('-')): tp - for tp in available_trading_pairs}.get(exchange_trading_pair) - return connector_trading_pair - - if not base or not quote: - return None - base = convert_from_exchange_symbol(base) - quote = convert_from_exchange_symbol(quote) - return f"{base}-{quote}" - - -def convert_to_exchange_trading_pair(hb_trading_pair: str, delimiter: str = "") -> str: - """ - Note: The result of this method can safely be used to submit/make queries. - Result shouldn't be used to parse responses as Kraken add special formating to most pairs. - """ - if "-" in hb_trading_pair: - base, quote = hb_trading_pair.split("-") - elif "/" in hb_trading_pair: - base, quote = hb_trading_pair.split("/") - else: - return hb_trading_pair - base = convert_to_exchange_symbol(base) - quote = convert_to_exchange_symbol(quote) - - exchange_trading_pair = f"{base}{delimiter}{quote}" - return exchange_trading_pair - - -def is_dark_pool(trading_pair_details: Dict[str, Any]): - ''' - Want to filter out dark pool trading pairs from the list of trading pairs - For more info, please check - https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool - ''' - if trading_pair_details.get('altname'): - return trading_pair_details.get('altname').endswith('.d') - return False - - -def _build_private_rate_limits(tier: KrakenAPITier = KrakenAPITier.STARTER) -> List[RateLimit]: - private_rate_limits = [] - - PRIVATE_ENDPOINT_LIMIT, MATCHING_ENGINE_LIMIT = CONSTANTS.KRAKEN_TIER_LIMITS[tier] - - # Private REST endpoints - private_rate_limits.extend([ - # Private API Pool - RateLimit( - limit_id=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID, - limit=PRIVATE_ENDPOINT_LIMIT, - time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, - ), - # Private endpoints - RateLimit( - limit_id=CONSTANTS.GET_TOKEN_PATH_URL, - limit=PRIVATE_ENDPOINT_LIMIT, - time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=CONSTANTS.BALANCE_PATH_URL, - limit=PRIVATE_ENDPOINT_LIMIT, - time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, - weight=2, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=CONSTANTS.OPEN_ORDERS_PATH_URL, - limit=PRIVATE_ENDPOINT_LIMIT, - time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, - weight=2, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], - ), - RateLimit( - limit_id=CONSTANTS.QUERY_ORDERS_PATH_URL, - limit=PRIVATE_ENDPOINT_LIMIT, - time_interval=CONSTANTS.PRIVATE_ENDPOINT_LIMIT_INTERVAL, - weight=2, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.PRIVATE_ENDPOINT_LIMIT_ID)], - ), - ]) - - # Matching Engine Limits - private_rate_limits.extend([ - RateLimit( - limit_id=CONSTANTS.ADD_ORDER_PATH_URL, - limit=MATCHING_ENGINE_LIMIT, - time_interval=CONSTANTS.MATCHING_ENGINE_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.MATCHING_ENGINE_LIMIT_ID)], - ), - RateLimit( - limit_id=CONSTANTS.CANCEL_ORDER_PATH_URL, - limit=MATCHING_ENGINE_LIMIT, - time_interval=CONSTANTS.MATCHING_ENGINE_LIMIT_INTERVAL, - linked_limits=[LinkedLimitWeightPair(CONSTANTS.MATCHING_ENGINE_LIMIT_ID)], - ), - ]) - - return private_rate_limits - - -def build_rate_limits_by_tier(tier: KrakenAPITier = KrakenAPITier.STARTER) -> List[RateLimit]: - rate_limits = [] - - rate_limits.extend(CONSTANTS.PUBLIC_API_LIMITS) - rate_limits.extend(_build_private_rate_limits(tier=tier)) - - return rate_limits - - -def _api_tier_validator(value: str) -> Optional[str]: - """ - Determines if input value is a valid API tier - """ - try: - KrakenAPITier(value.upper()) - except ValueError: - return "No such Kraken API Tier." - - -class KrakenConfigMap(BaseConnectorConfigMap): - connector: str = Field(default="kraken", client_data=None) - kraken_api_key: SecretStr = Field( - default=..., - client_data=ClientFieldData( - prompt=lambda cm: "Enter your Kraken API key", - is_secure=True, - is_connect_key=True, - prompt_on_new=True, - ) - ) - kraken_secret_key: SecretStr = Field( - default=..., - client_data=ClientFieldData( - prompt=lambda cm: "Enter your Kraken secret key", - is_secure=True, - is_connect_key=True, - prompt_on_new=True, - ) - ) - kraken_api_tier: str = Field( - default="Starter", - client_data=ClientFieldData( - prompt=lambda cm: "Enter your Kraken API Tier (Starter/Intermediate/Pro)", - is_connect_key=True, - prompt_on_new=True, - ) - ) - - class Config: - title = "kraken" - - -KEYS = KrakenConfigMap.construct() - - -def build_api_factory(throttler: AsyncThrottler) -> WebAssistantsFactory: - api_factory = WebAssistantsFactory(throttler=throttler) - return api_factory diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py index fb78678ca8..65e9844f75 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py @@ -3,7 +3,7 @@ import re import unittest from typing import Awaitable -from unittest.mock import AsyncMock, patch, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from aioresponses import aioresponses from bidict import bidict @@ -11,9 +11,9 @@ from hummingbot.client.config.client_config_map import ClientConfigMap from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils -from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant from hummingbot.core.api_throttler.async_throttler import AsyncThrottler diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index abe45e8a6f..57cb2b717c 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -3,7 +3,7 @@ import re import unittest from typing import Awaitable, Dict, List, Optional -from unittest.mock import AsyncMock, patch, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from aioresponses import aioresponses from bidict import bidict @@ -11,10 +11,10 @@ from hummingbot.client.config.client_config_map import ClientConfigMap from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant from hummingbot.core.api_throttler.async_throttler import AsyncThrottler @@ -65,6 +65,7 @@ def setUp(self) -> None: self.resume_test_event = asyncio.Event() self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair})) + def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() @@ -75,6 +76,7 @@ def handle(self, record): def _is_logged(self, log_level: str, message: str) -> bool: return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records) + def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py index 70452456e6..290ee5d7e1 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_auth.py @@ -3,9 +3,8 @@ import hashlib import hmac import json -from copy import copy from unittest import TestCase -from unittest.mock import MagicMock,patch +from unittest.mock import MagicMock, patch from typing_extensions import Awaitable diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index 0949286523..e90754b9f3 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -546,7 +546,7 @@ def configure_erroneous_http_fill_trade_response( mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) - regex_url = re.compile(url + r"\?.*") + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, status=400, callback=callback) return url @@ -601,7 +601,7 @@ def configure_partial_fill_trade_response( mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) - regex_url = re.compile(url + r"\?.*") + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_fills_request_partial_fill_mock_response(order=order) mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url @@ -612,7 +612,7 @@ def configure_full_fill_trade_response( mock_api: aioresponses, callback: Optional[Callable] = lambda *args, **kwargs: None) -> str: url = web_utils.private_rest_url(path_url=CONSTANTS.QUERY_TRADES_PATH_URL) - regex_url = re.compile(url + r"\?.*") + regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = self._order_fills_request_full_fill_mock_response(order=order) mock_api.post(regex_url, body=json.dumps(response), callback=callback) return url @@ -746,10 +746,10 @@ def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): "ordertype": "limit", "pair": "XBT/EUR", "postxid": "OGTT3Y-C6I3P-XRI6HX", - "price": order.price, + "price": str(order.price), "time": "1560516023.070651", "type": "sell", - "vol": order.amount + "vol": str(order.amount) } } ], @@ -759,6 +759,12 @@ def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): } ] + @aioresponses() + def test_lost_order_removed_if_not_found_during_order_status_update(self, mock_api): + # Disabling this test because the connector has not been updated yet to validate + # order not found during status update (check _is_order_not_found_during_status_update_error) + pass + @aioresponses() @patch("hummingbot.connector.time_synchronizer.TimeSynchronizer._current_seconds_counter") def test_update_time_synchronizer_successfully(self, mock_api, seconds_counter_mock): diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py index e30b15c4b2..35c953c0ca 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py @@ -2,8 +2,8 @@ from unittest import TestCase from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder -from hummingbot.core.data_type.common import OrderType, TradeType, PositionAction -from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate +from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType +from hummingbot.core.data_type.in_flight_order import OrderState class KrakenInFlightOrderTests(TestCase): diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py index c41042a205..52e3718677 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_order_book.py @@ -53,7 +53,7 @@ def test_diff_message_from_exchange(self): "1534614248.765567" ], ], - "update_id":3407459756 + "update_id": 3407459756 }, timestamp=1640000000, ) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py index fec2f7b3a7..73eeaf088a 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_utils.py @@ -30,11 +30,12 @@ def test_convert_to_exchange_trading_pair(self): self.assertEqual(self.ex_trading_pair, utils.convert_to_exchange_trading_pair(self.ex_trading_pair)) def test_split_to_base_quote(self): - self.assertEqual((self.hb_base_asset,self.quote_asset), utils.split_to_base_quote(self.trading_pair)) + self.assertEqual((self.hb_base_asset, self.quote_asset), utils.split_to_base_quote(self.trading_pair)) def test_convert_from_exchange_trading_pair(self): self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.trading_pair)) - self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_trading_pair,("BTC-USDT","ETH-USDT"))) + self.assertEqual(self.trading_pair, + utils.convert_from_exchange_trading_pair(self.ex_trading_pair, ("BTC-USDT", "ETH-USDT"))) self.assertEqual(self.trading_pair, utils.convert_from_exchange_trading_pair(self.ex_ws_trading_pair)) def test_build_rate_limits_by_tier(self): diff --git a/test/hummingbot/connector/exchange/kraken_v1/__init__.py b/test/hummingbot/connector/exchange/kraken_v1/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py deleted file mode 100644 index 55538894aa..0000000000 --- a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_order_book_data_source.py +++ /dev/null @@ -1,271 +0,0 @@ -import asyncio -import json -import re -import unittest -from decimal import Decimal -from typing import Awaitable, Dict, List -from unittest.mock import AsyncMock, patch - -from aioresponses import aioresponses - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource -from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier -from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler -from hummingbot.core.data_type.order_book import OrderBook, OrderBookMessage - - -class KrakenAPIOrderBookDataSourceTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.ev_loop = asyncio.get_event_loop() - cls.base_asset = "COINALPHA" - cls.quote_asset = "HBOT" - cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" - cls.api_tier = KrakenAPITier.STARTER - - def setUp(self) -> None: - super().setUp() - self.mocking_assistant = NetworkMockingAssistant() - self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) - self.data_source = KrakenAPIOrderBookDataSource(self.throttler, trading_pairs=[self.trading_pair]) - - def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): - ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) - return ret - - def get_last_traded_prices_mock(self, last_trade_close: Decimal) -> Dict: - last_traded_prices = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "a": [ - "52609.60000", - "1", - "1.000" - ], - "b": [ - "52609.50000", - "1", - "1.000" - ], - "c": [ - str(last_trade_close), - "0.00080000" - ], - "v": [ - "1920.83610601", - "7954.00219674" - ], - "p": [ - "52389.94668", - "54022.90683" - ], - "t": [ - 23329, - 80463 - ], - "l": [ - "51513.90000", - "51513.90000" - ], - "h": [ - "53219.90000", - "57200.00000" - ], - "o": "52280.40000" - } - } - } - return last_traded_prices - - def get_depth_mock(self) -> Dict: - depth = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "asks": [ - [ - "52523.00000", - "1.199", - 1616663113 - ], - [ - "52536.00000", - "0.300", - 1616663112 - ] - ], - "bids": [ - [ - "52522.90000", - "0.753", - 1616663112 - ], - [ - "52522.80000", - "0.006", - 1616663109 - ] - ] - } - } - } - return depth - - def get_public_asset_pair_mock(self) -> Dict: - asset_pairs = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "altname": f"{self.base_asset}{self.quote_asset}", - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 5, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [ - 2, - 3, - 4, - 5 - ], - "leverage_sell": [ - 2, - 3, - 4, - 5 - ], - "fees": [ - [ - 0, - 0.26 - ], - [ - 50000, - 0.24 - ], - ], - "fees_maker": [ - [ - 0, - 0.16 - ], - [ - 50000, - 0.14 - ], - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.005" - }, - } - } - return asset_pairs - - def get_trade_data_mock(self) -> List: - trade_data = [ - 0, - [ - [ - "5541.20000", - "0.15850568", - "1534614057.321597", - "s", - "l", - "" - ], - [ - "6060.00000", - "0.02455000", - "1534614057.324998", - "b", - "l", - "" - ] - ], - "trade", - f"{self.base_asset}/{self.quote_asset}" - ] - return trade_data - - @aioresponses() - def test_get_last_traded_prices(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - last_traded_price = Decimal("52641.10000") - resp = self.get_last_traded_prices_mock(last_trade_close=last_traded_price) - mocked_api.get(regex_url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout( - KrakenAPIOrderBookDataSource.get_last_traded_prices( - trading_pairs=[self.trading_pair], throttler=self.throttler - ) - ) - - self.assertIn(self.trading_pair, ret) - self.assertEqual(float(last_traded_price), ret[self.trading_pair]) - - @aioresponses() - def test_get_new_order_book(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_depth_mock() - mocked_api.get(regex_url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair)) - - self.assertTrue(isinstance(ret, OrderBook)) - - bids_df, asks_df = ret.snapshot - pair_data = resp["result"][f"X{self.base_asset}{self.quote_asset}"] - first_bid_price = float(pair_data["bids"][0][0]) - first_ask_price = float(pair_data["asks"][0][0]) - - self.assertEqual(first_bid_price, bids_df.iloc[0]["price"]) - self.assertEqual(first_ask_price, asks_df.iloc[0]["price"]) - - # @aioresponses() - # def test_fetch_trading_pairs(self, mocked_api): - # url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - # regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - # resp = self.get_public_asset_pair_mock() - # mocked_api.get(regex_url, body=json.dumps(resp)) - # - # resp = self.async_run_with_timeout(KrakenAPIOrderBookDataSource.fetch_trading_pairs(), 2) - # - # self.assertTrue(len(resp) == 1) - # self.assertIn(self.trading_pair, resp) - - @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) - def test_listen_for_trades(self, ws_connect_mock): - ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() - resp = self.get_trade_data_mock() - self.mocking_assistant.add_websocket_aiohttp_message( - websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) - ) - output_queue = asyncio.Queue() - - self.ev_loop.create_task(self.data_source.listen_for_trades(self.ev_loop, output_queue)) - self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value) - - self.assertTrue(not output_queue.empty()) - msg = output_queue.get_nowait() - self.assertTrue(isinstance(msg, OrderBookMessage)) - first_trade_price = resp[1][0][0] - self.assertEqual(msg.content["price"], first_trade_price) - - self.assertTrue(not output_queue.empty()) - msg = output_queue.get_nowait() - self.assertTrue(isinstance(msg, OrderBookMessage)) - second_trade_price = resp[1][1][0] - self.assertEqual(msg.content["price"], second_trade_price) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py deleted file mode 100644 index 7786f2ffa6..0000000000 --- a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_api_user_stream_data_source.py +++ /dev/null @@ -1,138 +0,0 @@ -import asyncio -import json -import re -import unittest -from typing import Awaitable, Dict, List -from unittest.mock import AsyncMock, patch - -from aioresponses import aioresponses - -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.connector.exchange.kraken.kraken_utils import build_rate_limits_by_tier -from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant -from hummingbot.core.api_throttler.async_throttler import AsyncThrottler - - -class KrakenAPIUserStreamDataSourceTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.ev_loop = asyncio.get_event_loop() - cls.base_asset = "COINALPHA" - cls.quote_asset = "HBOT" - cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" - cls.api_tier = KrakenAPITier.STARTER - - def setUp(self) -> None: - super().setUp() - self.mocking_assistant = NetworkMockingAssistant() - self.throttler = AsyncThrottler(build_rate_limits_by_tier(self.api_tier)) - not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" - kraken_auth = KrakenAuth(api_key="someKey", secret_key=not_a_real_secret) - self.data_source = KrakenAPIUserStreamDataSource(self.throttler, kraken_auth) - - def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): - ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) - return ret - - @staticmethod - def get_auth_response_mock() -> Dict: - auth_resp = { - "error": [], - "result": { - "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", - "expires": 900 - } - } - return auth_resp - - @staticmethod - def get_open_orders_mock() -> List: - open_orders = [ - [ - { - "OGTT3Y-C6I3P-XRI6HX": { - "status": "closed" - } - }, - { - "OGTT3Y-C6I3P-XRI6HX": { - "status": "closed" - } - } - ], - "openOrders", - { - "sequence": 59342 - } - ] - return open_orders - - @staticmethod - def get_own_trades_mock() -> List: - own_trades = [ - [ - { - "TDLH43-DVQXD-2KHVYY": { - "cost": "1000000.00000", - "fee": "1600.00000", - "margin": "0.00000", - "ordertxid": "TDLH43-DVQXD-2KHVYY", - "ordertype": "limit", - "pair": "XBT/EUR", - "postxid": "OGTT3Y-C6I3P-XRI6HX", - "price": "100000.00000", - "time": "1560516023.070651", - "type": "sell", - "vol": "1000000000.00000000" - } - }, - ], - "ownTrades", - { - "sequence": 2948 - } - ] - return own_trades - - @aioresponses() - def test_get_auth_token(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_auth_response_mock() - mocked_api.post(regex_url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout(self.data_source.get_auth_token()) - - self.assertEqual(ret, resp["result"]["token"]) - - @aioresponses() - @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) - def test_listen_for_user_stream(self, mocked_api, ws_connect_mock): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_auth_response_mock() - mocked_api.post(regex_url, body=json.dumps(resp)) - - ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() - output_queue = asyncio.Queue() - self.ev_loop.create_task(self.data_source.listen_for_user_stream(output_queue)) - - resp = self.get_open_orders_mock() - self.mocking_assistant.add_websocket_aiohttp_message( - websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) - ) - ret = self.async_run_with_timeout(coroutine=output_queue.get()) - - self.assertEqual(ret, resp) - - resp = self.get_own_trades_mock() - self.mocking_assistant.add_websocket_aiohttp_message( - websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) - ) - ret = self.async_run_with_timeout(coroutine=output_queue.get()) - - self.assertEqual(ret, resp) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py deleted file mode 100644 index 6096c57f47..0000000000 --- a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_exchange.py +++ /dev/null @@ -1,454 +0,0 @@ -import asyncio -import json -import re -import unittest -from decimal import Decimal -from functools import partial -from typing import Awaitable, Dict - -from aioresponses import aioresponses - -from hummingbot.client.config.client_config_map import ClientConfigMap -from hummingbot.client.config.config_helpers import ClientConfigAdapter -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS -from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange -from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrderNotCreated -from hummingbot.connector.test_support.network_mocking_assistant import NetworkMockingAssistant -from hummingbot.core.clock import Clock, ClockMode -from hummingbot.core.data_type.common import OrderType, TradeType -from hummingbot.core.event.event_logger import EventLogger -from hummingbot.core.event.events import ( - BuyOrderCompletedEvent, - BuyOrderCreatedEvent, - MarketEvent, - OrderCancelledEvent, - SellOrderCreatedEvent, -) -from hummingbot.core.network_iterator import NetworkStatus - - -class KrakenExchangeTest(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - super().setUpClass() - cls.ev_loop = asyncio.get_event_loop() - cls.base_asset = "COINALPHA" - cls.quote_asset = "HBOT" - cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" - - def setUp(self) -> None: - super().setUp() - self.mocking_assistant = NetworkMockingAssistant() - self.event_listener = EventLogger() - not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" - self.client_config_map = ClientConfigAdapter(ClientConfigMap()) - - self.exchange = KrakenExchange( - client_config_map=self.client_config_map, - kraken_api_key="someKey", - kraken_secret_key=not_a_real_secret, - trading_pairs=[self.trading_pair], - ) - self.start_time = 1 - self.clock = Clock(clock_mode=ClockMode.BACKTEST, start_time=self.start_time) - - def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): - ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) - return ret - - def simulate_trading_rules_initialized(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) - - self.async_run_with_timeout(self.exchange._update_trading_rules(), timeout=2) - - @staticmethod - def register_sent_request(requests_list, url, **kwargs): - requests_list.append((url, kwargs)) - - def get_asset_pairs_mock(self) -> Dict: - asset_pairs = { - "error": [], - "result": { - f"X{self.base_asset}{self.quote_asset}": { - "altname": f"{self.base_asset}{self.quote_asset}", - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": f"{self.base_asset}", - "aclass_quote": "currency", - "quote": f"{self.quote_asset}", - "lot": "unit", - "pair_decimals": 5, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [ - 2, - 3, - ], - "leverage_sell": [ - 2, - 3, - ], - "fees": [ - [ - 0, - 0.26 - ], - [ - 50000, - 0.24 - ], - ], - "fees_maker": [ - [ - 0, - 0.16 - ], - [ - 50000, - 0.14 - ], - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.005" - }, - } - } - return asset_pairs - - def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: - balances = { - "error": [], - "result": { - self.base_asset: str(base_asset_balance), - self.quote_asset: str(quote_asset_balance), - "USDT": "171288.6158", - } - } - return balances - - def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: - open_orders = { - "error": [], - "result": { - "open": { - "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), - } - } - } - return open_orders - - def get_query_orders_mock( - self, exchange_id: str, quantity: float, price: float, order_type: str, status: str - ) -> Dict: - query_orders = { - "error": [], - "result": { - exchange_id: self.get_order_status_mock(quantity, price, order_type, status) - } - } - return query_orders - - def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: - order_status = { - "refid": None, - "userref": 0, - "status": status, - "opentm": 1616666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": { - "pair": f"{self.base_asset}{self.quote_asset}", - "type": order_type, - "ordertype": "limit", - "price": str(price), - "price2": "0", - "leverage": "none", - "order": f"buy {quantity} {self.base_asset}{self.quote_asset} @ limit {price}", - "close": "" - }, - "vol": str(quantity), - "vol_exec": "0", - "cost": str(price * quantity), - "fee": "0.00000", - "price": str(price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [ - "TCCCTY-WE2O6-P3NB37" - ] - } - return order_status - - def get_order_placed_mock(self, exchange_id: str, quantity: float, price: float, order_type: str) -> Dict: - order_placed = { - "error": [], - "result": { - "descr": { - "order": f"{order_type} {quantity} {self.base_asset}{self.quote_asset}" - f" @ limit {price} with 2:1 leverage", - }, - "txid": [ - exchange_id - ] - } - } - return order_placed - - @aioresponses() - def test_get_asset_pairs(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout(self.exchange.get_asset_pairs()) - - self.assertIn(self.trading_pair, ret) - self.assertEqual( - ret[self.trading_pair], resp["result"][f"X{self.base_asset}{self.quote_asset}"] # shallow comparison is ok - ) - - @aioresponses() - def test_update_balances(self, mocked_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" - resp = self.get_asset_pairs_mock() - mocked_api.get(url, body=json.dumps(resp)) - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.BALANCE_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_balances_mock(base_asset_balance=10, quote_asset_balance=20) - mocked_api.post(regex_url, body=json.dumps(resp)) - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.async_run_with_timeout(self.exchange._update_balances()) - - self.assertEqual(self.exchange.available_balances[self.quote_asset], Decimal("18")) - - @aioresponses() - def test_update_order_status_order_closed(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) - self.exchange.add_listener(MarketEvent.BuyOrderCompleted, self.event_listener) - - self.async_run_with_timeout(self.exchange._update_order_status()) - - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCompletedEvent)) - self.assertNotIn(order_id, self.exchange.in_flight_orders) - - @aioresponses() - def test_check_network_success(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - resp = {"status": 200, "result": []} - mock_api.get(url, body=json.dumps(resp)) - - ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) - - self.assertEqual(ret, NetworkStatus.CONNECTED) - - @aioresponses() - def test_check_network_raises_cancelled_error(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - mock_api.get(url, exception=asyncio.CancelledError) - - with self.assertRaises(asyncio.CancelledError): - self.async_run_with_timeout(coroutine=self.exchange.check_network()) - - @aioresponses() - def test_check_network_not_connected_for_error_status(self, mock_api): - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TIME_PATH_URL}" - resp = {"status": 405, "result": []} - mock_api.get(url, status=405, body=json.dumps(resp)) - - ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) - - self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) - - @aioresponses() - def test_get_open_orders_with_userref(self, mocked_api): - sent_messages = [] - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.OPEN_ORDERS_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_open_orders_mock(quantity=1, price=2, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) - userref = 1 - - ret = self.async_run_with_timeout(self.exchange.get_open_orders_with_userref(userref)) - - self.assertEqual(len(sent_messages), 1) - - sent_message = sent_messages[0][1]["data"] - - self.assertEqual(sent_message["userref"], userref) - self.assertEqual(ret, resp["result"]) # shallow comparison ok - - @aioresponses() - def test_get_order(self, mocked_api): - sent_messages = [] - order_id = "someId" - exchange_id = "someExchangeId" - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.QUERY_ORDERS_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_query_orders_mock(exchange_id, quantity=1, price=2, order_type="buy", status="closed") - mocked_api.post(regex_url, body=json.dumps(resp), callback=partial(self.register_sent_request, sent_messages)) - - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) - ret = self.async_run_with_timeout(self.exchange.get_order(client_order_id=order_id)) - - self.assertEqual(len(sent_messages), 1) - - sent_message = sent_messages[0][1]["data"] - - self.assertEqual(sent_message["txid"], exchange_id) - self.assertEqual(ret, resp["result"]) # shallow comparison ok - - @aioresponses() - def test_execute_buy(self, mocked_api): - self.exchange.start(self.clock, self.start_time) - self.simulate_trading_rules_initialized(mocked_api) - - order_id = "someId" - exchange_id = "someExchangeId" - userref = 1 - quantity = 1 - price = 2 - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="buy") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.add_listener(MarketEvent.BuyOrderCreated, self.event_listener) - self.async_run_with_timeout( - self.exchange.execute_buy( - order_id, - self.trading_pair, - amount=Decimal(quantity), - order_type=OrderType.LIMIT, - price=Decimal(price), - userref=userref, - ) - ) - - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], BuyOrderCreatedEvent)) - self.assertIn(order_id, self.exchange.in_flight_orders) - - @aioresponses() - def test_execute_sell(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" - userref = 1 - quantity = 1 - price = 2 - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ADD_ORDER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = self.get_order_placed_mock(exchange_id, quantity, price, order_type="sell") - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.start(self.clock, self.start_time) - self.simulate_trading_rules_initialized(mocked_api) - self.exchange.add_listener(MarketEvent.SellOrderCreated, self.event_listener) - self.async_run_with_timeout( - self.exchange.execute_sell( - order_id, - self.trading_pair, - amount=Decimal(quantity), - order_type=OrderType.LIMIT, - price=Decimal(price), - userref=userref, - ) - ) - - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], SellOrderCreatedEvent)) - self.assertIn(order_id, self.exchange.in_flight_orders) - - @aioresponses() - def test_execute_cancel(self, mocked_api): - order_id = "someId" - exchange_id = "someExchangeId" - - url = f"{CONSTANTS.BASE_URL}{CONSTANTS.CANCEL_ORDER_PATH_URL}" - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) - resp = { - "error": [], - "result": { - "count": 1 - } - } - mocked_api.post(regex_url, body=json.dumps(resp)) - - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) - self.exchange.in_flight_orders[order_id].update_exchange_order_id(exchange_id) - self.exchange.in_flight_orders[order_id].last_state = "pending" - self.exchange.add_listener(MarketEvent.OrderCancelled, self.event_listener) - ret = self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) - - self.assertEqual(len(self.event_listener.event_log), 1) - self.assertTrue(isinstance(self.event_listener.event_log[0], OrderCancelledEvent)) - self.assertNotIn(order_id, self.exchange.in_flight_orders) - self.assertEqual(ret["origClientOrderId"], order_id) - - def test_execute_cancel_ignores_local_orders(self): - order_id = "someId" - exchange_id = "someExchangeId" - - self.exchange.start_tracking_order( - order_id=order_id, - exchange_order_id=exchange_id, - trading_pair=self.trading_pair, - trade_type=TradeType.BUY, - price=2, - amount=1, - order_type=OrderType.LIMIT, - userref=1, - ) - - with self.assertRaises(KrakenInFlightOrderNotCreated): - self.async_run_with_timeout(self.exchange.execute_cancel(self.trading_pair, order_id)) diff --git a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py deleted file mode 100644 index 6de2297f5f..0000000000 --- a/test/hummingbot/connector/exchange/kraken_v1/test_kraken_in_flight_order.py +++ /dev/null @@ -1,90 +0,0 @@ -from decimal import Decimal -from unittest import TestCase - -from hummingbot.connector.exchange.kraken.kraken_in_flight_order import KrakenInFlightOrder -from hummingbot.core.data_type.common import OrderType, TradeType - - -class KrakenInFlightOrderTests(TestCase): - def test_order_is_local_after_creation(self): - order = KrakenInFlightOrder( - client_order_id="someId", - exchange_order_id=None, - trading_pair="BTC-USDT", - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal(45000), - amount=Decimal(1), - creation_timestamp=1640001112.0, - userref=1, - ) - - self.assertTrue(order.is_local) - - def test_serialize_order_to_json(self): - order = KrakenInFlightOrder( - client_order_id="OID1", - exchange_order_id="EOID1", - trading_pair="COINALPHA-HBOT", - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal(1000), - amount=Decimal(1), - creation_timestamp=1640001112.0, - userref=2, - initial_state="OPEN", - ) - - expected_json = { - "client_order_id": order.client_order_id, - "exchange_order_id": order.exchange_order_id, - "trading_pair": order.trading_pair, - "order_type": order.order_type.name, - "trade_type": order.trade_type.name, - "price": str(order.price), - "amount": str(order.amount), - "last_state": order.last_state, - "executed_amount_base": str(order.executed_amount_base), - "executed_amount_quote": str(order.executed_amount_quote), - "fee_asset": order.fee_asset, - "fee_paid": str(order.fee_paid), - "creation_timestamp": 1640001112.0, - "userref": order.userref, - } - - self.assertEqual(expected_json, order.to_json()) - - def test_deserialize_order_from_json(self): - json = { - "client_order_id": "OID1", - "exchange_order_id": "EOID1", - "trading_pair": "COINALPHA-HBOT", - "order_type": OrderType.LIMIT.name, - "trade_type": TradeType.BUY.name, - "price": "1000", - "amount": "1", - "last_state": "OPEN", - "executed_amount_base": "0.1", - "executed_amount_quote": "110", - "fee_asset": "BNB", - "fee_paid": "10", - "creation_timestamp": 1640001112.0, - "userref": 2, - } - - order: KrakenInFlightOrder = KrakenInFlightOrder.from_json(json) - - self.assertEqual(json["client_order_id"], order.client_order_id) - self.assertEqual(json["exchange_order_id"], order.exchange_order_id) - self.assertEqual(json["trading_pair"], order.trading_pair) - self.assertEqual(OrderType.LIMIT, order.order_type) - self.assertEqual(TradeType.BUY, order.trade_type) - self.assertEqual(Decimal(json["price"]), order.price) - self.assertEqual(Decimal(json["amount"]), order.amount) - self.assertEqual(Decimal(json["executed_amount_base"]), order.executed_amount_base) - self.assertEqual(Decimal(json["executed_amount_quote"]), order.executed_amount_quote) - self.assertEqual(json["fee_asset"], order.fee_asset) - self.assertEqual(Decimal(json["fee_paid"]), order.fee_paid) - self.assertEqual(json["last_state"], order.last_state) - self.assertEqual(json["creation_timestamp"], order.creation_timestamp) - self.assertEqual(json["userref"], order.userref) From 8e9e1aed09ffafc9cbb43a302475c2ab371d695b Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 07:23:55 +0800 Subject: [PATCH 15/34] format code --- .../kraken_api_order_book_data_source.py | 2 +- .../kraken_api_user_stream_data_source.py | 2 +- .../connector/exchange/kraken/kraken_auth.py | 8 +-- .../exchange/kraken/kraken_constants.py | 8 +-- .../exchange/kraken/kraken_exchange.py | 58 ++++++++----------- .../exchange/kraken/kraken_in_fight_order.py | 2 +- .../exchange/kraken/kraken_order_book.py | 10 +--- .../connector/exchange/kraken/kraken_utils.py | 2 +- 8 files changed, 36 insertions(+), 56 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py index 71153c13ac..0ac5fdc603 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py @@ -2,8 +2,8 @@ import time from typing import TYPE_CHECKING, Any, Dict, List, Optional -from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils +from hummingbot.connector.exchange.kraken.kraken_order_book import KrakenOrderBook from hummingbot.connector.exchange.kraken.kraken_utils import ( convert_from_exchange_trading_pair, convert_to_exchange_trading_pair, diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index 73c96a048f..7915bfe268 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -1,5 +1,5 @@ import asyncio -from typing import Any, Dict, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Optional from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index bc31fc8de0..342ea33f58 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -1,13 +1,9 @@ -from typing import ( - Optional, - Dict, - Any -) import base64 import hashlib import hmac -import time import json +import time +from typing import Any, Dict, Optional from hummingbot.connector.time_synchronizer import TimeSynchronizer from hummingbot.core.web_assistant.auth import AuthBase diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index 4d05b54b93..f7b7b23fc7 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -1,9 +1,7 @@ from enum import Enum -from typing import ( - Dict, - Tuple, -) -from hummingbot.core.api_throttler.data_types import RateLimit, LinkedLimitWeightPair +from typing import Dict, Tuple + +from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit from hummingbot.core.data_type.in_flight_order import OrderState DEFAULT_DOMAIN = "kraken" diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index fb5fdccf37..04bfdecb96 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -1,26 +1,23 @@ import asyncio +import re from collections import defaultdict from decimal import Decimal from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple -import re from bidict import bidict from hummingbot.connector.constants import s_decimal_NaN -from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, \ - kraken_web_utils as web_utils +from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils +from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource +from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource +from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth +from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier +from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder from hummingbot.connector.exchange.kraken.kraken_utils import ( build_rate_limits_by_tier, convert_from_exchange_symbol, convert_from_exchange_trading_pair, ) -from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.connector.exchange.kraken.kraken_api_order_book_data_source import KrakenAPIOrderBookDataSource -from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource -from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth -from hummingbot.connector.exchange.kraken.kraken_in_fight_order import ( - KrakenInFlightOrder, -) from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule from hummingbot.connector.utils import combine_to_hb_trading_pair, get_new_client_order_id @@ -28,7 +25,7 @@ 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.order_book_tracker_data_source import OrderBookTrackerDataSource -from hummingbot.core.data_type.trade_fee import DeductedFromReturnsTradeFee, TokenAmount, TradeFeeBase +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.utils.async_utils import safe_ensure_future from hummingbot.core.utils.estimate_fee import build_trade_fee @@ -149,7 +146,7 @@ def _is_request_exception_related_to_time_synchronizer(self, request_exception: return False def _is_order_not_found_during_status_update_error(self, status_update_exception: Exception) -> bool: - return str(CONSTANTS.ORDER_NOT_EXIST_ERROR_CODE) in str(status_update_exception) + return False def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: return False @@ -355,15 +352,9 @@ async def _place_order(self, data=data, is_auth_required=True) - # todo - # o_order_result = order_result['response']["data"]["statuses"][0] - # if "error" in o_order_result: - # raise IOError(f"Error submitting order {userref}: {o_order_result['error']}") - # o_data = o_order_result.get("resting") or o_order_result.get("filled") o_id = order_result["txid"][0] return (o_id, self.current_timestamp) - # todo async def _api_request_with_retry(self, method: RESTMethod, endpoint: str, @@ -407,7 +398,8 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): data=api_params, is_auth_required=True) if isinstance(cancel_result, dict) and ( - cancel_result.get("result",{}).get("count") == 1 or cancel_result.get("result",{}).get("error") is not None): + cancel_result.get("result", {}).get("count") == 1 or cancel_result.get("result", {}).get( + "error") is not None): return True return False @@ -495,12 +487,17 @@ async def _user_stream_event_listener(self): ] async for event_message in self._iter_user_event_queue(): try: - channel: str = event_message[-2] - results: List[Any] = event_message[0] - if channel == CONSTANTS.USER_TRADES_ENDPOINT_NAME: - self._process_trade_message(results) - elif channel == CONSTANTS.USER_ORDERS_ENDPOINT_NAME: - self._process_order_message(event_message) + if isinstance(event_message, list): + channel: str = event_message[-2] + results: List[Any] = event_message[0] + if channel == CONSTANTS.USER_TRADES_ENDPOINT_NAME: + self._process_trade_message(results) + elif channel == CONSTANTS.USER_ORDERS_ENDPOINT_NAME: + self._process_order_message(event_message) + elif event_message is asyncio.CancelledError: + raise asyncio.CancelledError + else: + raise Exception(event_message) except asyncio.CancelledError: raise except Exception: @@ -542,15 +539,10 @@ def _process_trade_message(self, trades: List): trade: Dict[str, str] = update[trade_id] trade["trade_id"] = trade_id exchange_order_id = trade.get("ordertxid") - try: - client_order_id = next(key for key, value in self.in_flight_orders.items() - if value.exchange_order_id == exchange_order_id) - except StopIteration: - continue - tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) + tracked_order = self._order_tracker.all_fillable_orders_by_exchange_order_id.get(exchange_order_id) if tracked_order is None: - self.logger().debug(f"Ignoring trade message with id {client_order_id}: not in in_flight_orders.") + self.logger().debug(f"Ignoring trade message with id {exchange_order_id}: not in in_flight_orders.") else: trade_update = self._create_trade_update_with_order_fill_data( order_fill=trade, @@ -591,7 +583,7 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade is_auth_required=True) for trade_id, trade_fill in all_fills_response["result"].items(): - trade: Dict[str, str] = all_fills_response[trade_id] + trade: Dict[str, str] = all_fills_response["result"][trade_id] trade["trade_id"] = trade_id trade_update = self._create_trade_update_with_order_fill_data( order_fill=trade, diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index 2533a53f81..7e5ff075c0 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -1,10 +1,10 @@ import copy +import math from decimal import Decimal from typing import Any, Dict, Optional, Tuple from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate -import math class KrakenInFlightOrder(InFlightOrder): diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book.py b/hummingbot/connector/exchange/kraken/kraken_order_book.py index ac98bf43ea..0c17770a12 100644 --- a/hummingbot/connector/exchange/kraken/kraken_order_book.py +++ b/hummingbot/connector/exchange/kraken/kraken_order_book.py @@ -1,14 +1,8 @@ -from typing import ( - Dict, - Optional -) +from typing import Dict, Optional from hummingbot.core.data_type.common import TradeType from hummingbot.core.data_type.order_book import OrderBook -from hummingbot.core.data_type.order_book_message import ( - OrderBookMessage, - OrderBookMessageType -) +from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType class KrakenOrderBook(OrderBook): diff --git a/hummingbot/connector/exchange/kraken/kraken_utils.py b/hummingbot/connector/exchange/kraken/kraken_utils.py index 266bdb5c6c..2c93bcc353 100644 --- a/hummingbot/connector/exchange/kraken/kraken_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_utils.py @@ -38,7 +38,7 @@ def split_to_base_quote(exchange_trading_pair: str) -> Tuple[Optional[str], Opti def convert_from_exchange_trading_pair(exchange_trading_pair: str, available_trading_pairs: Optional[Tuple] = None) -> \ -Optional[str]: + Optional[str]: base, quote = "", "" if "-" in exchange_trading_pair: base, quote = split_to_base_quote(exchange_trading_pair) From b619089bc434fb0fd5487a6d0c5d283adfb68d5c Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 07:33:01 +0800 Subject: [PATCH 16/34] format code --- .../exchange/kraken/kraken_exchange.py | 8 +- .../exchange/kraken/kraken_in_fight_order.py | 6 +- .../exchange/kraken/test_kraken_exchange.py | 81 ++++++++++--------- 3 files changed, 45 insertions(+), 50 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 04bfdecb96..41fa885fa4 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -398,8 +398,8 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): data=api_params, is_auth_required=True) if isinstance(cancel_result, dict) and ( - cancel_result.get("result", {}).get("count") == 1 or cancel_result.get("result", {}).get( - "error") is not None): + cancel_result.get("result", {}).get("count") == 1 or + cancel_result.get("result", {}).get("error") is not None): return True return False @@ -481,10 +481,6 @@ async def _user_stream_event_listener(self): Listens to messages from _user_stream_tracker.user_stream queue. Traders, Orders, and Balance updates from the WS. """ - user_channels = [ - CONSTANTS.USER_TRADES_ENDPOINT_NAME, - CONSTANTS.USER_ORDERS_ENDPOINT_NAME, - ] async for event_message in self._iter_user_event_queue(): try: if isinstance(event_message, list): diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py index 7e5ff075c0..fd859e94b7 100644 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py @@ -41,12 +41,10 @@ def __init__( @property def is_done(self) -> bool: - return ( - self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} + return (self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} or math.isclose(self.executed_amount_base, self.amount) or self.executed_amount_base >= self.amount - # or self. - ) + ) @property def attributes(self) -> Tuple[Any]: diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index e90754b9f3..c9d316a734 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -123,46 +123,47 @@ def all_symbols_request_mock_response(self): "error": [], "result": { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): + { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } } From b73b5830e2a406a2772d0e007e98a37fceb94bd5 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 07:40:15 +0800 Subject: [PATCH 17/34] format code --- .../exchange/kraken/test_kraken_exchange.py | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index c9d316a734..fa97dc6913 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -119,53 +119,52 @@ def balance_event_websocket_update(self): @property def all_symbols_request_mock_response(self): - return { + response = { "error": [], - "result": - { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): - { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + "result": { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" } + } } + return response @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: From e3341b0984c443b1a85b4ea21d20b6fabcf5fa86 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 18:00:37 +0800 Subject: [PATCH 18/34] fix auth bug --- hummingbot/connector/exchange/kraken/kraken_auth.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index 342ea33f58..9faf76406b 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -4,6 +4,8 @@ import json import time from typing import Any, Dict, Optional +from urllib.parse import urlparse + from hummingbot.connector.time_synchronizer import TimeSynchronizer from hummingbot.core.web_assistant.auth import AuthBase @@ -25,16 +27,13 @@ def get_tracking_nonce(self) -> str: return str(self._last_tracking_nonce) async def rest_authenticate(self, request: RESTRequest) -> RESTRequest: - headers = {} - if request.headers is not None: - headers.update(request.headers) data = json.loads(request.data) if request.data is not None else {} + _path = urlparse(request.url).path - auth_dict: Dict[str, Any] = self._generate_auth_dict(request.url, data) - headers.update(auth_dict["headers"]) - request.headers = headers - request.data = json.dumps(auth_dict["postDict"]) + auth_dict: Dict[str, Any] = self._generate_auth_dict(_path, data) + request.headers = auth_dict["headers"] + request.data = auth_dict["postDict"] return request async def ws_authenticate(self, request: WSRequest) -> WSRequest: From 4a6c34ab7b9eca927fc478b6a955885bd1ae6e75 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 18:10:21 +0800 Subject: [PATCH 19/34] fix auth bug --- hummingbot/connector/exchange/kraken/kraken_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_auth.py b/hummingbot/connector/exchange/kraken/kraken_auth.py index 9faf76406b..574401a8f6 100755 --- a/hummingbot/connector/exchange/kraken/kraken_auth.py +++ b/hummingbot/connector/exchange/kraken/kraken_auth.py @@ -6,7 +6,6 @@ from typing import Any, Dict, Optional from urllib.parse import urlparse - from hummingbot.connector.time_synchronizer import TimeSynchronizer from hummingbot.core.web_assistant.auth import AuthBase from hummingbot.core.web_assistant.connections.data_types import RESTRequest, WSRequest From 37cae7118aa549c931ce3eded28d8d9bad911e5b Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 20 Feb 2024 18:10:54 +0800 Subject: [PATCH 20/34] format code --- .../connector/exchange/kraken/test_kraken_exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index fa97dc6913..fb2bc5c3f6 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -452,11 +452,11 @@ def create_exchange_instance(self): def validate_auth_credentials_present(self, request_call: RequestCall): self._validate_auth_credentials_taking_parameters_from_argument( request_call_tuple=request_call, - params=json.loads(request_call.kwargs["data"]) + params=request_call.kwargs["data"] ) def validate_order_creation_request(self, order: InFlightOrder, request_call: RequestCall): - request_data = json.loads(request_call.kwargs["data"]) + request_data = request_call.kwargs["data"] self.assertEqual(self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), request_data["pair"]) self.assertEqual(order.trade_type.name.lower(), request_data["type"]) self.assertEqual(KrakenExchange.kraken_order_type(OrderType.LIMIT), request_data["ordertype"]) @@ -464,7 +464,7 @@ def validate_order_creation_request(self, order: InFlightOrder, request_call: Re self.assertEqual(Decimal("10000"), Decimal(request_data["price"])) def validate_order_cancelation_request(self, order: InFlightOrder, request_call: RequestCall): - request_data = json.loads(request_call.kwargs["data"]) + request_data = request_call.kwargs["data"] self.assertEqual(order.exchange_order_id, request_data["txid"]) def validate_order_status_request(self, order: InFlightOrder, request_call: RequestCall): @@ -472,7 +472,7 @@ def validate_order_status_request(self, order: InFlightOrder, request_call: Requ self.assertEqual(order.exchange_order_id, request_params["txid"]) def validate_trades_request(self, order: InFlightOrder, request_call: RequestCall): - request_params = json.loads(request_call.kwargs["data"]) + request_params = request_call.kwargs["data"] self.assertEqual(order.exchange_order_id, str(request_params["txid"])) def configure_order_not_found_error_cancelation_response( From 1125b048794a4e400935fbd40f8c345454e8d73a Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 22 Feb 2024 02:39:26 +0800 Subject: [PATCH 21/34] fix bug --- .../kraken_api_order_book_data_source.py | 2 +- .../kraken_api_user_stream_data_source.py | 12 +- .../exchange/kraken/kraken_exchange.py | 107 +++++++++++------- .../exchange/kraken/kraken_web_utils.py | 7 ++ .../test_kraken_api_order_book_data_source.py | 2 +- ...test_kraken_api_user_stream_data_source.py | 5 +- .../exchange/kraken/test_kraken_exchange.py | 58 +--------- 7 files changed, 82 insertions(+), 111 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py index 0ac5fdc603..89fbc329b1 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_order_book_data_source.py @@ -103,7 +103,7 @@ async def _subscribe_channels(self, ws: WSAssistant): trades_payload = { "event": "subscribe", "pair": trading_pairs, - "subscription": {"name": 'trade', "depth": 1000}, + "subscription": {"name": 'trade'}, } subscribe_trade_request: WSJSONRequest = WSJSONRequest(payload=trades_payload) diff --git a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py index 7915bfe268..9f436560cf 100755 --- a/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py +++ b/hummingbot/connector/exchange/kraken/kraken_api_user_stream_data_source.py @@ -41,16 +41,8 @@ async def get_auth_token(self) -> str: response_json = await self._connector._api_post(path_url=CONSTANTS.GET_TOKEN_PATH_URL, params={}, is_auth_required=True) except Exception: - raise IOError(f"Error parsing data from {CONSTANTS.GET_TOKEN_PATH_URL}.") - - err = response_json["error"] - if "EAPI:Invalid nonce" in err: - self.logger().error(f"Invalid nonce error from {CONSTANTS.GET_TOKEN_PATH_URL}. " + - "Please ensure your Kraken API key nonce window is at least 10, " + - "and if needed reset your API key.") - raise IOError({"error": response_json}) - - return response_json["result"]["token"] + raise + return response_json["token"] async def _subscribe_channels(self, websocket_assistant: WSAssistant): """ diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 41fa885fa4..df479fd393 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -128,10 +128,6 @@ def is_trading_required(self) -> bool: def supported_order_types(self): return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] - # async def get_all_pairs_prices(self) -> List[Dict[str, str]]: - # pairs_prices = await self._api_get(path_url=CONSTANTS.TICKER_BOOK_PATH_URL) - # return pairs_prices - def _build_async_throttler(self, api_tier: KrakenAPITier) -> AsyncThrottler: limits_pct = self._client_config.rate_limits_share_pct if limits_pct < Decimal("100"): @@ -189,6 +185,22 @@ def _get_fee(self, ) return trade_base_fee + async def _api_get(self, *args, **kwargs): + kwargs["method"] = RESTMethod.GET + return await self._api_request_with_retry(*args, **kwargs) + + async def _api_post(self, *args, **kwargs): + kwargs["method"] = RESTMethod.POST + return await self._api_request_with_retry(*args, **kwargs) + + async def _api_put(self, *args, **kwargs): + kwargs["method"] = RESTMethod.PUT + return await self._api_request_with_retry(*args, **kwargs) + + async def _api_delete(self, *args, **kwargs): + kwargs["method"] = RESTMethod.DELETE + return await self._api_request_with_retry(*args, **kwargs) + def generate_userref(self): self._last_userref += 1 return self._last_userref @@ -276,9 +288,9 @@ def sell(self, async def get_asset_pairs(self) -> Dict[str, Any]: if not self._asset_pairs: - asset_pairs = await self._api_request(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) + asset_pairs = await self._api_request_with_retry(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) self._asset_pairs = {f"{details['base']}-{details['quote']}": details - for _, details in asset_pairs["result"].items() if + for _, details in asset_pairs.items() if web_utils.is_exchange_information_valid(details)} return self._asset_pairs @@ -357,35 +369,50 @@ async def _place_order(self, async def _api_request_with_retry(self, method: RESTMethod, - endpoint: str, + path_url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, is_auth_required: bool = False, retry_interval=2.0) -> Dict[str, Any]: + response_json = None result = None for retry_attempt in range(self.REQUEST_ATTEMPTS): try: - result = await self._api_request(path_url=endpoint, method=method, params=params, data=data, + response_json = await self._api_request(path_url=path_url, method=method, params=params, data=data, is_auth_required=is_auth_required) + # if "EOrder:Unknown order" in err or "EOrder:Invalid order" in err: + # return {"error": err} + # elif "EAPI:Invalid nonce" in err: + # self.logger().error(f"Invalid nonce error from {path_url}. " + + # "Please ensure your Kraken API key nonce window is at least 10, " + + # "and if needed reset your API key.") + if response_json.get("error") and "EAPI:Invalid nonce" in response_json.get("error", ""): + self.logger().error(f"Invalid nonce error from {path_url}. " + + "Please ensure your Kraken API key nonce window is at least 10, " + + "and if needed reset your API key.") + result = response_json.get("result") + if not result or response_json.get("error"): + self.logger().error(f"Error received from {path_url}. Response is {response_json}.") + raise IOError({"error": response_json}) break except IOError as e: if self.is_cloudflare_exception(e): - if endpoint == CONSTANTS.ADD_ORDER_PATH_URL: - self.logger().info(f"Retrying {endpoint}") + if path_url == CONSTANTS.ADD_ORDER_PATH_URL: + self.logger().info(f"Retrying {path_url}") # Order placement could have been successful despite the IOError, so check for the open order. response = await self.get_open_orders_with_userref(data.get('userref')) if any(response.get("open").values()): return response self.logger().warning( f"Cloudflare error. Attempt {retry_attempt + 1}/{self.REQUEST_ATTEMPTS}" - f" API command {method}: {endpoint}" + f" API command {method}: {path_url}" ) await asyncio.sleep(retry_interval ** retry_attempt) continue else: raise e - if result is None: - raise IOError(f"Error fetching data from {endpoint}.") + if not result: + raise IOError(f"Error fetching data from {path_url}, msg is {response_json}.") return result async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): @@ -394,12 +421,12 @@ async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): } cancel_result = await self._api_request_with_retry( method=RESTMethod.POST, - endpoint=CONSTANTS.CANCEL_ORDER_PATH_URL, + path_url=CONSTANTS.CANCEL_ORDER_PATH_URL, data=api_params, is_auth_required=True) if isinstance(cancel_result, dict) and ( - cancel_result.get("result", {}).get("count") == 1 or - cancel_result.get("result", {}).get("error") is not None): + cancel_result.get("count") == 1 or + cancel_result.get("error") is not None): return True return False @@ -450,7 +477,7 @@ async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> Lis } """ retval: list = [] - trading_pair_rules = exchange_info_dict["result"].values() + trading_pair_rules = exchange_info_dict.values() # for trading_pair, rule in asset_pairs_dict.items(): for rule in filter(web_utils.is_exchange_information_valid, trading_pair_rules): try: @@ -557,15 +584,16 @@ def _create_order_update_with_order_status_data(self, order_status: Dict[str, An def _process_order_message(self, orders: List): update = orders[0] - for exchange_order_id, order_msg in update[0].items(): - tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) - if not tracked_order: - self.logger().debug( - f"Ignoring order message with id {tracked_order.client_order_id}: not in in_flight_orders.") - return - order_update = self._create_order_update_with_order_status_data(order_status=order_msg, - order=tracked_order) - self._order_tracker.process_order_update(order_update=order_update) + for message in update: + for exchange_order_id, order_msg in message.items(): + tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) + if not tracked_order: + self.logger().debug( + f"Ignoring order message with id {order_msg}: not in in_flight_orders.") + return + order_update = self._create_order_update_with_order_status_data(order_status=order_msg, + order=tracked_order) + self._order_tracker.process_order_update(order_update=order_update) async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: trade_updates = [] @@ -574,12 +602,12 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade exchange_order_id = await order.get_exchange_order_id() all_fills_response = await self._api_request_with_retry( method=RESTMethod.POST, - endpoint=CONSTANTS.QUERY_TRADES_PATH_URL, + path_url=CONSTANTS.QUERY_TRADES_PATH_URL, data={"txid": exchange_order_id}, is_auth_required=True) - for trade_id, trade_fill in all_fills_response["result"].items(): - trade: Dict[str, str] = all_fills_response["result"][trade_id] + for trade_id, trade_fill in all_fills_response.items(): + trade: Dict[str, str] = all_fills_response[trade_id] trade["trade_id"] = trade_id trade_update = self._create_trade_update_with_order_fill_data( order_fill=trade, @@ -589,17 +617,19 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade except asyncio.TimeoutError: raise IOError(f"Skipped order update with order fills for {order.client_order_id} " "- waiting for exchange order id.") - + except Exception as e: + if "EOrder:Unknown order" in e or "EOrder:Invalid order" in e: + return trade_updates return trade_updates async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: updated_order_data = await self._api_request_with_retry( method=RESTMethod.POST, - endpoint=CONSTANTS.QUERY_ORDERS_PATH_URL, + path_url=CONSTANTS.QUERY_ORDERS_PATH_URL, params={"txid": tracked_order.exchange_order_id}, is_auth_required=True) - update = updated_order_data["result"].get(tracked_order.exchange_order_id) + update = updated_order_data.get(tracked_order.exchange_order_id) if update.get("error") is not None and "EOrder:Invalid order" not in update["error"]: self.logger().debug(f"Error in fetched status update for order {tracked_order.client_order_id}: " @@ -627,7 +657,7 @@ async def _update_balances(self): locked = defaultdict(Decimal) - for order in open_orders["result"].get("open").values(): + for order in open_orders.get("open").values(): if order.get("status") == "open": details = order.get("descr") if details.get("ordertype") == "limit": @@ -641,7 +671,7 @@ async def _update_balances(self): elif details.get("type") == "buy": locked[convert_from_exchange_symbol(quote)] += vol_locked * Decimal(details.get("price")) - for asset_name, balance in balances["result"].items(): + for asset_name, balance in balances.items(): cleaned_name = convert_from_exchange_symbol(asset_name).upper() total_balance = Decimal(balance) free_balance = total_balance - Decimal(locked[cleaned_name]) @@ -656,19 +686,18 @@ async def _update_balances(self): def _initialize_trading_pair_symbols_from_exchange_info(self, exchange_info: Dict[str, Any]): mapping = bidict() - for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info["result"].values()): - mapping[symbol_data["altname"]] = combine_to_hb_trading_pair(base=symbol_data["base"], - quote=symbol_data["quote"]) + for symbol_data in filter(web_utils.is_exchange_information_valid, exchange_info.values()): + mapping[symbol_data["altname"]] = convert_from_exchange_trading_pair(symbol_data["wsname"]) self._set_trading_pair_symbol_map(mapping) async def _get_last_traded_price(self, trading_pair: str) -> float: params = { "pair": await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) } - resp_json = await self._api_request( + resp_json = await self._api_request_with_retry( method=RESTMethod.GET, path_url=CONSTANTS.TICKER_PATH_URL, params=params ) - record = list(resp_json["result"].values())[0] + record = list(resp_json.values())[0] return float(record["c"][0]) diff --git a/hummingbot/connector/exchange/kraken/kraken_web_utils.py b/hummingbot/connector/exchange/kraken/kraken_web_utils.py index fb49f9be55..a42de263d3 100644 --- a/hummingbot/connector/exchange/kraken/kraken_web_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_web_utils.py @@ -1,3 +1,4 @@ +import time from typing import Optional import hummingbot.connector.exchange.kraken.kraken_constants as CONSTANTS @@ -44,3 +45,9 @@ def is_exchange_information_valid(trading_pair_details) -> bool: if trading_pair_details.get('altname'): return not trading_pair_details.get('altname').endswith('.d') return True + +async def get_current_server_time( + throttler, + domain +) -> float: + return time.time() \ No newline at end of file diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py index 65e9844f75..0f7c37655e 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py @@ -220,7 +220,7 @@ def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(self, ws_ expected_trade_subscription = { "event": "subscribe", "pair": [self.ws_ex_trading_pairs], - "subscription": {"name": 'trade', "depth": 1000}, + "subscription": {"name": 'trade'}, } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index 57cb2b717c..8fb2be7fc7 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -84,12 +84,9 @@ def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): @staticmethod def get_auth_response_mock() -> Dict: auth_resp = { - "error": [], - "result": { "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", "expires": 900 } - } return auth_resp @staticmethod @@ -150,7 +147,7 @@ def test_get_auth_token(self, mocked_api): ret = self.async_run_with_timeout(self.data_source.get_auth_token()) - self.assertEqual(ret, resp["result"]["token"]) + self.assertEqual(ret, resp["token"]) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index fb2bc5c3f6..88fce6d872 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -71,8 +71,6 @@ def balance_url(self): @property def latest_prices_request_mock_response(self): return { - "error": [], - "result": { self.ex_trading_pair: { "a": [ "30300.10000", @@ -111,7 +109,6 @@ def latest_prices_request_mock_response(self): "o": "30502.80000" } } - } @property def balance_event_websocket_update(self): @@ -120,8 +117,6 @@ def balance_event_websocket_update(self): @property def all_symbols_request_mock_response(self): response = { - "error": [], - "result": { self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), "wsname": f"{self.base_asset}/{self.quote_asset}", @@ -163,14 +158,11 @@ def all_symbols_request_mock_response(self): "ordermin": "0.0002" } } - } return response @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: response = { - "error": [], - "result": { self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), "wsname": f"{self.base_asset}/{self.quote_asset}", @@ -252,7 +244,6 @@ def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: "ordermin": "0.0002" } } - } return "INVALID-PAIR", response @property @@ -262,8 +253,6 @@ def network_status_request_successful_mock_response(self): @property def trading_rules_request_mock_response(self): return { - "error": [], - "result": { self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), "wsname": f"{self.base_asset}/{self.quote_asset}", @@ -305,13 +294,10 @@ def trading_rules_request_mock_response(self): "ordermin": "0.0002" } } - } @property def trading_rules_request_erroneous_mock_response(self): return { - "error": [], - "result": { "XBTUSDT": { "altname": "XBTUSDT", "wsname": "XBT/USDT", @@ -349,7 +335,6 @@ def trading_rules_request_erroneous_mock_response(self): "margin_stop": 40, } } - } @property def order_creation_request_successful_mock_response(self): @@ -365,21 +350,15 @@ def order_creation_request_successful_mock_response(self): @property def balance_request_mock_response_for_base_and_quote(self): return { - "error": [], - "result": { self.base_asset: str(10), self.quote_asset: str(2000), } - } @property def balance_request_mock_response_only_base(self): return { - "error": [], - "result": { self.base_asset: str(10), } - } @property def expected_latest_price(self): @@ -391,7 +370,7 @@ def expected_supported_order_types(self): @property def expected_trading_rule(self): - rule = list(self.trading_rules_request_mock_response["result"].values())[0] + rule = list(self.trading_rules_request_mock_response.values())[0] min_order_size = Decimal(rule.get('ordermin', 0)) min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") @@ -404,7 +383,7 @@ def expected_trading_rule(self): @property def expected_logged_error_for_erroneous_trading_rule(self): - erroneous_rule = list(self.trading_rules_request_erroneous_mock_response["result"].values())[0] + erroneous_rule = list(self.trading_rules_request_erroneous_mock_response.values())[0] return f"Error parsing the trading pair rule {erroneous_rule}. Skipping." @property @@ -813,8 +792,6 @@ def test_update_order_status_when_failed(self, mock_api): regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) order_status = { - "error": [], - "result": { order.exchange_order_id: { "refid": "None", "userref": 0, @@ -835,7 +812,6 @@ def test_update_order_status_when_failed(self, mock_api): "trades": [] } } - } mock_response = order_status mock_api.post(regex_url, body=json.dumps(mock_response)) @@ -968,8 +944,6 @@ def test_restore_tracking_states_only_registers_open_orders(self): def get_asset_pairs_mock(self) -> Dict: asset_pairs = { - "error": [], - "result": { f"X{self.base_asset}{self.quote_asset}": { "altname": f"{self.base_asset}{self.quote_asset}", "wsname": f"{self.base_asset}/{self.quote_asset}", @@ -1015,29 +989,22 @@ def get_asset_pairs_mock(self) -> Dict: "ordermin": "0.005" }, } - } return asset_pairs def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: balances = { - "error": [], - "result": { self.base_asset: str(base_asset_balance), self.quote_asset: str(quote_asset_balance), "USDT": "171288.6158", } - } return balances def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: open_orders = { - "error": [], - "result": { "open": { "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), } } - } return open_orders def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: @@ -1095,16 +1062,11 @@ def test_update_balances(self, mocked_api): def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return { - "error": [], - "result": { "count": 1 } - } def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: return { - "error": [], - "result": { order.exchange_order_id: { "refid": "None", "userref": 0, @@ -1125,12 +1087,9 @@ def _order_status_request_completely_filled_mock_response(self, order: InFlightO "trades": [] } } - } def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { - "error": [], - "result": { order.exchange_order_id: { "refid": "None", "userref": 0, @@ -1151,12 +1110,9 @@ def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> "trades": [] } } - } def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: return { - "error": [], - "result": { order.exchange_order_id: { "refid": "None", "userref": 0, @@ -1177,12 +1133,9 @@ def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: "trades": [] } } - } def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: return { - "error": [], - "result": { order.exchange_order_id: { "refid": "None", "userref": 0, @@ -1203,12 +1156,9 @@ def _order_status_request_partially_filled_mock_response(self, order: InFlightOr "trades": [] } } - } def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): return { - "error": [], - "result": { self.expected_fill_trade_id: { "ordertxid": order.exchange_order_id, "postxid": "TKH2SE-M7IF5-CFI7LT", @@ -1226,12 +1176,9 @@ def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): "maker": "true" } } - } def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): return { - "error": [], - "result": { self.expected_fill_trade_id: { "ordertxid": order.exchange_order_id, "postxid": "TKH2SE-M7IF5-CFI7LT", @@ -1249,4 +1196,3 @@ def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): "maker": "true" } } - } From 508ecc372b656eba76b769ef43df91cc3aec62dc Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 22 Feb 2024 02:46:38 +0800 Subject: [PATCH 22/34] format code --- .../exchange/kraken/kraken_exchange.py | 12 +- .../exchange/kraken/kraken_web_utils.py | 3 +- .../exchange/kraken/test_kraken_exchange.py | 830 +++++++++--------- 3 files changed, 420 insertions(+), 425 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index df479fd393..5f58aaa365 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -20,7 +20,7 @@ ) from hummingbot.connector.exchange_py_base import ExchangePyBase from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import combine_to_hb_trading_pair, get_new_client_order_id +from hummingbot.connector.utils import get_new_client_order_id from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderUpdate, TradeUpdate @@ -379,13 +379,8 @@ async def _api_request_with_retry(self, for retry_attempt in range(self.REQUEST_ATTEMPTS): try: response_json = await self._api_request(path_url=path_url, method=method, params=params, data=data, - is_auth_required=is_auth_required) - # if "EOrder:Unknown order" in err or "EOrder:Invalid order" in err: - # return {"error": err} - # elif "EAPI:Invalid nonce" in err: - # self.logger().error(f"Invalid nonce error from {path_url}. " + - # "Please ensure your Kraken API key nonce window is at least 10, " + - # "and if needed reset your API key.") + is_auth_required=is_auth_required) + if response_json.get("error") and "EAPI:Invalid nonce" in response_json.get("error", ""): self.logger().error(f"Invalid nonce error from {path_url}. " + "Please ensure your Kraken API key nonce window is at least 10, " + @@ -478,7 +473,6 @@ async def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> Lis """ retval: list = [] trading_pair_rules = exchange_info_dict.values() - # for trading_pair, rule in asset_pairs_dict.items(): for rule in filter(web_utils.is_exchange_information_valid, trading_pair_rules): try: trading_pair = await self.trading_pair_associated_to_exchange_symbol(symbol=rule.get("altname")) diff --git a/hummingbot/connector/exchange/kraken/kraken_web_utils.py b/hummingbot/connector/exchange/kraken/kraken_web_utils.py index a42de263d3..5f5011d406 100644 --- a/hummingbot/connector/exchange/kraken/kraken_web_utils.py +++ b/hummingbot/connector/exchange/kraken/kraken_web_utils.py @@ -46,8 +46,9 @@ def is_exchange_information_valid(trading_pair_details) -> bool: return not trading_pair_details.get('altname').endswith('.d') return True + async def get_current_server_time( throttler, domain ) -> float: - return time.time() \ No newline at end of file + return time.time() diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index 88fce6d872..7cf5891697 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -71,44 +71,44 @@ def balance_url(self): @property def latest_prices_request_mock_response(self): return { - self.ex_trading_pair: { - "a": [ - "30300.10000", - "1", - "1.000" - ], - "b": [ - "30300.00000", - "1", - "1.000" - ], - "c": [ - self.expected_latest_price, - "0.00067643" - ], - "v": [ - "4083.67001100", - "4412.73601799" - ], - "p": [ - "30706.77771", - "30689.13205" - ], - "t": [ - 34619, - 38907 - ], - "l": [ - "29868.30000", - "29868.30000" - ], - "h": [ - "31631.00000", - "31631.00000" - ], - "o": "30502.80000" - } + self.ex_trading_pair: { + "a": [ + "30300.10000", + "1", + "1.000" + ], + "b": [ + "30300.00000", + "1", + "1.000" + ], + "c": [ + self.expected_latest_price, + "0.00067643" + ], + "v": [ + "4083.67001100", + "4412.73601799" + ], + "p": [ + "30706.77771", + "30689.13205" + ], + "t": [ + 34619, + 38907 + ], + "l": [ + "29868.30000", + "29868.30000" + ], + "h": [ + "31631.00000", + "31631.00000" + ], + "o": "30502.80000" } + } @property def balance_event_websocket_update(self): @@ -117,133 +117,133 @@ def balance_event_websocket_update(self): @property def all_symbols_request_mock_response(self): response = { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" } + } return response @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: response = { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - }, - "ETHUSDT.d": { - "altname": "ETHUSDT.d", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + }, + "ETHUSDT.d": { + "altname": "ETHUSDT.d", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" } + } return "INVALID-PAIR", response @property @@ -253,88 +253,88 @@ def network_status_request_successful_mock_response(self): @property def trading_rules_request_mock_response(self): return { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" - } + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" } + } @property def trading_rules_request_erroneous_mock_response(self): return { - "XBTUSDT": { - "altname": "XBTUSDT", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - } + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, } + } @property def order_creation_request_successful_mock_response(self): @@ -350,15 +350,15 @@ def order_creation_request_successful_mock_response(self): @property def balance_request_mock_response_for_base_and_quote(self): return { - self.base_asset: str(10), - self.quote_asset: str(2000), - } + self.base_asset: str(10), + self.quote_asset: str(2000), + } @property def balance_request_mock_response_only_base(self): return { - self.base_asset: str(10), - } + self.base_asset: str(10), + } @property def expected_latest_price(self): @@ -792,26 +792,26 @@ def test_update_order_status_when_failed(self, mock_api): regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) order_status = { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "expired", - "opentm": 1499827319.559, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": "1.0", - "vol_exec": "0.0", - "cost": "11253.7", - "fee": "0.00000", - "price": "10000.0", - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "expired", + "opentm": 1499827319.559, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": "1.0", + "vol_exec": "0.0", + "cost": "11253.7", + "fee": "0.00000", + "price": "10000.0", + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } + } mock_response = order_status mock_api.post(regex_url, body=json.dumps(mock_response)) @@ -944,67 +944,67 @@ def test_restore_tracking_states_only_registers_open_orders(self): def get_asset_pairs_mock(self) -> Dict: asset_pairs = { - f"X{self.base_asset}{self.quote_asset}": { - "altname": f"{self.base_asset}{self.quote_asset}", - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": f"{self.base_asset}", - "aclass_quote": "currency", - "quote": f"{self.quote_asset}", - "lot": "unit", - "pair_decimals": 5, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [ - 2, - 3, + f"X{self.base_asset}{self.quote_asset}": { + "altname": f"{self.base_asset}{self.quote_asset}", + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": f"{self.base_asset}", + "aclass_quote": "currency", + "quote": f"{self.quote_asset}", + "lot": "unit", + "pair_decimals": 5, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [ + 2, + 3, + ], + "leverage_sell": [ + 2, + 3, + ], + "fees": [ + [ + 0, + 0.26 ], - "leverage_sell": [ - 2, - 3, + [ + 50000, + 0.24 ], - "fees": [ - [ - 0, - 0.26 - ], - [ - 50000, - 0.24 - ], + ], + "fees_maker": [ + [ + 0, + 0.16 ], - "fees_maker": [ - [ - 0, - 0.16 - ], - [ - 50000, - 0.14 - ], + [ + 50000, + 0.14 ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.005" - }, - } + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.005" + }, + } return asset_pairs def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: balances = { - self.base_asset: str(base_asset_balance), - self.quote_asset: str(quote_asset_balance), - "USDT": "171288.6158", - } + self.base_asset: str(base_asset_balance), + self.quote_asset: str(quote_asset_balance), + "USDT": "171288.6158", + } return balances def get_open_orders_mock(self, quantity: float, price: float, order_type: str) -> Dict: open_orders = { - "open": { - "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), - } + "open": { + "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), } + } return open_orders def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: @@ -1062,137 +1062,137 @@ def test_update_balances(self, mocked_api): def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return { - "count": 1 - } + "count": 1 + } def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "closed", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": str(order.amount), - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "closed", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } + } def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "canceled", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": "0", - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "canceled", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } + } def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "open", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": "0", - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } + } def _order_status_request_partially_filled_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "open", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": str(order.amount / 2), - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] - } + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "open", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount / 2), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] } + } def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): return { - self.expected_fill_trade_id: { - "ordertxid": order.exchange_order_id, - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(self.expected_partial_fill_price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(self.expected_partial_fill_amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" - } + self.expected_fill_trade_id: { + "ordertxid": order.exchange_order_id, + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(self.expected_partial_fill_price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(self.expected_partial_fill_amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" } + } def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): return { - self.expected_fill_trade_id: { - "ordertxid": order.exchange_order_id, - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(order.price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(order.amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" - } + self.expected_fill_trade_id: { + "ordertxid": order.exchange_order_id, + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(order.price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(order.amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" } + } From 6344f5be00592043250d4a140df69547fd8d4578 Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 22 Feb 2024 02:50:07 +0800 Subject: [PATCH 23/34] format code --- .../kraken/test_kraken_api_user_stream_data_source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index 8fb2be7fc7..beee197869 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -84,9 +84,9 @@ def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): @staticmethod def get_auth_response_mock() -> Dict: auth_resp = { - "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", - "expires": 900 - } + "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", + "expires": 900 + } return auth_resp @staticmethod From 7e9978be347c49ddf7adcfb83ca639e61d5421f9 Mon Sep 17 00:00:00 2001 From: bczhang Date: Fri, 23 Feb 2024 02:16:47 +0800 Subject: [PATCH 24/34] fix oserror bug --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 5f58aaa365..addc2d2c1f 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -612,18 +612,19 @@ async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[Trade raise IOError(f"Skipped order update with order fills for {order.client_order_id} " "- waiting for exchange order id.") except Exception as e: - if "EOrder:Unknown order" in e or "EOrder:Invalid order" in e: + if "EOrder:Unknown order" in str(e) or "EOrder:Invalid order" in str(e): return trade_updates return trade_updates async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate: + exchange_order_id = await tracked_order.get_exchange_order_id() updated_order_data = await self._api_request_with_retry( method=RESTMethod.POST, path_url=CONSTANTS.QUERY_ORDERS_PATH_URL, - params={"txid": tracked_order.exchange_order_id}, + params={"txid": exchange_order_id}, is_auth_required=True) - update = updated_order_data.get(tracked_order.exchange_order_id) + update = updated_order_data.get(exchange_order_id) if update.get("error") is not None and "EOrder:Invalid order" not in update["error"]: self.logger().debug(f"Error in fetched status update for order {tracked_order.client_order_id}: " @@ -633,7 +634,7 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda order_update = OrderUpdate( client_order_id=tracked_order.client_order_id, - exchange_order_id=tracked_order.exchange_order_id, + exchange_order_id=exchange_order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=new_state, From e0d0b4684dad066e1b8452fed645c96c355359ff Mon Sep 17 00:00:00 2001 From: bczhang Date: Fri, 23 Feb 2024 03:37:02 +0800 Subject: [PATCH 25/34] fix Unexpected error while processing event 201 --- .../connector/exchange/kraken/kraken_constants.py | 2 +- .../connector/exchange/kraken/kraken_exchange.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index f7b7b23fc7..58062795ef 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -55,7 +55,7 @@ class KrakenAPITier(Enum): # Order States ORDER_STATE = { - "pending": OrderState.PENDING_CREATE, + "pending": OrderState.OPEN, "open": OrderState.OPEN, "closed": OrderState.FILLED, "canceled": OrderState.CANCELED, diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index addc2d2c1f..4ab3adf7db 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -387,7 +387,6 @@ async def _api_request_with_retry(self, "and if needed reset your API key.") result = response_json.get("result") if not result or response_json.get("error"): - self.logger().error(f"Error received from {path_url}. Response is {response_json}.") raise IOError({"error": response_json}) break except IOError as e: @@ -411,8 +410,9 @@ async def _api_request_with_retry(self, return result async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): + exchange_order_id = tracked_order.get_exchange_order_id() api_params = { - "txid": tracked_order.exchange_order_id, + "txid": exchange_order_id, } cancel_result = await self._api_request_with_retry( method=RESTMethod.POST, @@ -621,15 +621,10 @@ async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpda updated_order_data = await self._api_request_with_retry( method=RESTMethod.POST, path_url=CONSTANTS.QUERY_ORDERS_PATH_URL, - params={"txid": exchange_order_id}, + data={"txid": exchange_order_id}, is_auth_required=True) update = updated_order_data.get(exchange_order_id) - - if update.get("error") is not None and "EOrder:Invalid order" not in update["error"]: - self.logger().debug(f"Error in fetched status update for order {tracked_order.client_order_id}: " - f"{update['error']}") - await self._place_cancel(tracked_order.client_order_id, tracked_order) new_state = CONSTANTS.ORDER_STATE[update["status"]] order_update = OrderUpdate( From 0c5e7221fd363e25cce09f5b729741e178905c60 Mon Sep 17 00:00:00 2001 From: bczhang Date: Mon, 26 Feb 2024 13:23:20 +0800 Subject: [PATCH 26/34] fix bug --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 4ab3adf7db..a039126c64 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -410,7 +410,7 @@ async def _api_request_with_retry(self, return result async def _place_cancel(self, order_id: str, tracked_order: InFlightOrder): - exchange_order_id = tracked_order.get_exchange_order_id() + exchange_order_id = await tracked_order.get_exchange_order_id() api_params = { "txid": exchange_order_id, } From e23bcd62c120ac681557acbfb53caff6966e0313 Mon Sep 17 00:00:00 2001 From: bczhang Date: Tue, 27 Feb 2024 00:20:22 +0800 Subject: [PATCH 27/34] fix order status bug --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index a039126c64..41e3031632 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -585,9 +585,10 @@ def _process_order_message(self, orders: List): self.logger().debug( f"Ignoring order message with id {order_msg}: not in in_flight_orders.") return - order_update = self._create_order_update_with_order_status_data(order_status=order_msg, - order=tracked_order) - self._order_tracker.process_order_update(order_update=order_update) + if "status" in order_msg: + order_update = self._create_order_update_with_order_status_data(order_status=order_msg, + order=tracked_order) + self._order_tracker.process_order_update(order_update=order_update) async def _all_trade_updates_for_order(self, order: InFlightOrder) -> List[TradeUpdate]: trade_updates = [] From 9e1f0616345bf2d3c280a916d8f11e3b572d3840 Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 29 Feb 2024 01:31:25 +0800 Subject: [PATCH 28/34] fix trade issue --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 41e3031632..11c7320eb8 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -540,7 +540,7 @@ def _create_trade_update_with_order_fill_data( trade_update = TradeUpdate( trade_id=str(order_fill["trade_id"]), client_order_id=order.client_order_id, - exchange_order_id=order.exchange_order_id, + exchange_order_id=order_fill.get("ordertxid"), trading_pair=order.trading_pair, fee=fee, fill_base_amount=Decimal(order_fill["vol"]), From 9808862b6988baa8ac92a343eacfcbb6e31c5d1b Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 29 Feb 2024 22:56:29 +0800 Subject: [PATCH 29/34] fix trade issue --- .../exchange/kraken/kraken_constants.py | 2 + .../exchange/kraken/kraken_exchange.py | 23 +- .../test_kraken_api_order_book_data_source.py | 28 +- ...test_kraken_api_user_stream_data_source.py | 9 +- .../exchange/kraken/test_kraken_exchange.py | 458 +++++++++++------- 5 files changed, 316 insertions(+), 204 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index 58062795ef..6e895cf59d 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -53,6 +53,8 @@ class KrakenAPITier(Enum): QUERY_ORDERS_PATH_URL = "/0/private/QueryOrders" QUERY_TRADES_PATH_URL = "/0/private/QueryTrades" + +UNKNOWN_ORDER_MESSAGE = "Unknown order" # Order States ORDER_STATE = { "pending": OrderState.OPEN, diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 11c7320eb8..50bbf14185 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -145,7 +145,7 @@ def _is_order_not_found_during_status_update_error(self, status_update_exception return False def _is_order_not_found_during_cancelation_error(self, cancelation_exception: Exception) -> bool: - return False + return CONSTANTS.UNKNOWN_ORDER_MESSAGE in str(cancelation_exception) def _create_web_assistants_factory(self) -> WebAssistantsFactory: return web_utils.build_api_factory( @@ -288,7 +288,8 @@ def sell(self, async def get_asset_pairs(self) -> Dict[str, Any]: if not self._asset_pairs: - asset_pairs = await self._api_request_with_retry(method=RESTMethod.GET, path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) + asset_pairs = await self._api_request_with_retry(method=RESTMethod.GET, + path_url=CONSTANTS.ASSET_PAIRS_PATH_URL) self._asset_pairs = {f"{details['base']}-{details['quote']}": details for _, details in asset_pairs.items() if web_utils.is_exchange_information_valid(details)} @@ -556,10 +557,22 @@ def _process_trade_message(self, trades: List): trade: Dict[str, str] = update[trade_id] trade["trade_id"] = trade_id exchange_order_id = trade.get("ordertxid") - + _userref = trade.get("userref") tracked_order = self._order_tracker.all_fillable_orders_by_exchange_order_id.get(exchange_order_id) - if tracked_order is None: - self.logger().debug(f"Ignoring trade message with id {exchange_order_id}: not in in_flight_orders.") + + if not tracked_order: + all_orders = self._order_tracker.all_fillable_orders + for k, v in all_orders.items(): + if v.userref == _userref: + tracked_order = v + break + if not tracked_order: + self.logger().debug(f"Ignoring trade message with id {exchange_order_id}: not in in_flight_orders.") + else: + trade_update = self._create_trade_update_with_order_fill_data( + order_fill=trade, + order=tracked_order) + self._order_tracker.process_trade_update(trade_update) else: trade_update = self._create_trade_update_with_order_fill_data( order_fill=trade, diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py index 0f7c37655e..65f040071f 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_order_book_data_source.py @@ -160,7 +160,7 @@ def _snapshot_response(self): @aioresponses() def test_get_new_order_book_successful(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}?pair={self.ex_trading_pair}".replace(".", r"\.").replace("?", r"\?")) resp = self._snapshot_response() @@ -181,7 +181,7 @@ def test_get_new_order_book_successful(self, mock_api): @aioresponses() def test_get_new_order_book_raises_exception(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}?pair={self.ex_trading_pair}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): @@ -276,13 +276,13 @@ def test_subscribe_channels_raises_exception_and_logs_error(self): self.async_run_with_timeout(self.listening_task) self.assertTrue( - self._is_logged("ERROR", "Unexpected error occurred subscribing to order book trading and delta streams...") + self._is_logged("ERROR", "Unexpected error occurred subscribing to order book data streams.") ) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() - self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -300,7 +300,7 @@ def test_listen_for_trades_logs_exception(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] - self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -319,7 +319,7 @@ def test_listen_for_trades_logs_exception(self): def test_listen_for_trades_successful(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [self._trade_update_event(), asyncio.CancelledError()] - self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -328,12 +328,12 @@ def test_listen_for_trades_successful(self): msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) - self.assertEqual(1534614057.324998, msg.trade_id) + self.assertEqual(1534614057.321597, msg.trade_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() - self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -351,7 +351,7 @@ def test_listen_for_order_book_diffs_logs_exception(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] - self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -371,7 +371,7 @@ def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() diff_event = self._order_diff_event() mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] - self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue + self.data_source._message_queue[self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() @@ -380,12 +380,12 @@ def test_listen_for_order_book_diffs_successful(self): msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) - self.assertEqual(int(diff_event[1]["a"][0][0][2]), msg.update_id) + self.assertEqual(diff_event[1]["a"][0][2], str(msg.update_id)) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}?pair={self.ex_trading_pair}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError, repeat=True) @@ -402,7 +402,7 @@ def test_listen_for_order_book_snapshots_log_exception(self, mock_api, sleep_moc sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError()) url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}?pair={self.ex_trading_pair}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception, repeat=True) @@ -418,7 +418,7 @@ def test_listen_for_order_book_snapshots_log_exception(self, mock_api, sleep_moc def test_listen_for_order_book_snapshots_successful(self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) - regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) + regex_url = re.compile(f"^{url}?pair={self.ex_trading_pair}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py index beee197869..57cb2b717c 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_api_user_stream_data_source.py @@ -84,8 +84,11 @@ def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): @staticmethod def get_auth_response_mock() -> Dict: auth_resp = { - "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", - "expires": 900 + "error": [], + "result": { + "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", + "expires": 900 + } } return auth_resp @@ -147,7 +150,7 @@ def test_get_auth_token(self, mocked_api): ret = self.async_run_with_timeout(self.data_source.get_auth_token()) - self.assertEqual(ret, resp["token"]) + self.assertEqual(ret, resp["result"]["token"]) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index 7cf5891697..c3c19175ff 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -20,6 +20,7 @@ from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.event.events import MarketOrderFailureEvent +from hummingbot.core.network_iterator import NetworkStatus class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): @@ -71,42 +72,45 @@ def balance_url(self): @property def latest_prices_request_mock_response(self): return { - self.ex_trading_pair: { - "a": [ - "30300.10000", - "1", - "1.000" - ], - "b": [ - "30300.00000", - "1", - "1.000" - ], - "c": [ - self.expected_latest_price, - "0.00067643" - ], - "v": [ - "4083.67001100", - "4412.73601799" - ], - "p": [ - "30706.77771", - "30689.13205" - ], - "t": [ - 34619, - 38907 - ], - "l": [ - "29868.30000", - "29868.30000" - ], - "h": [ - "31631.00000", - "31631.00000" - ], - "o": "30502.80000" + "error": [], + "result": { + self.ex_trading_pair: { + "a": [ + "30300.10000", + "1", + "1.000" + ], + "b": [ + "30300.00000", + "1", + "1.000" + ], + "c": [ + self.expected_latest_price, + "0.00067643" + ], + "v": [ + "4083.67001100", + "4412.73601799" + ], + "p": [ + "30706.77771", + "30689.13205" + ], + "t": [ + 34619, + 38907 + ], + "l": [ + "29868.30000", + "29868.30000" + ], + "h": [ + "31631.00000", + "31631.00000" + ], + "o": "30502.80000" + } } } @@ -158,7 +162,11 @@ def all_symbols_request_mock_response(self): "ordermin": "0.0002" } } - return response + result = { + "error": [], + "result": response + } + return result @property def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: @@ -248,110 +256,161 @@ def all_symbols_including_invalid_pair_mock_response(self) -> Tuple[str, Any]: @property def network_status_request_successful_mock_response(self): - return {} + return { + "error": [], + "result": { + "a": [ + "30300.10000", + "1", + "1.000" + ], + "b": [ + "30300.00000", + "1", + "1.000" + ], + "c": [ + "30303.20000", + "0.00067643" + ], + "v": [ + "4083.67001100", + "4412.73601799" + ], + "p": [ + "30706.77771", + "30689.13205" + ], + "t": [ + 34619, + 38907 + ], + "l": [ + "29868.30000", + "29868.30000" + ], + "h": [ + "31631.00000", + "31631.00000" + ], + "o": "30502.80000" + } + } @property def trading_rules_request_mock_response(self): return { - self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { - "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), - "wsname": f"{self.base_asset}/{self.quote_asset}", - "aclass_base": "currency", - "base": self.base_asset, - "aclass_quote": "currency", - "quote": self.quote_asset, - "lot": "unit", - "pair_decimals": 1, - "lot_decimals": 8, - "lot_multiplier": 1, - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, - "ordermin": "0.0002" + "error": [], + "result": { + self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset): { + "altname": self.exchange_symbol_for_tokens(self.base_asset, self.quote_asset), + "wsname": f"{self.base_asset}/{self.quote_asset}", + "aclass_base": "currency", + "base": self.base_asset, + "aclass_quote": "currency", + "quote": self.quote_asset, + "lot": "unit", + "pair_decimals": 1, + "lot_decimals": 8, + "lot_multiplier": 1, + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + "ordermin": "0.0002" + } } } @property def trading_rules_request_erroneous_mock_response(self): return { - "XBTUSDT": { - "altname": "XBTUSDT", - "wsname": "XBT/USDT", - "aclass_base": "currency", - "base": "XXBT", - "aclass_quote": "currency", - "quote": "USDT", - "lot": "unit", - "leverage_buy": [2, 3], - "leverage_sell": [2, 3], - "fees": [ - [0, 0.26], - [50000, 0.24], - [100000, 0.22], - [250000, 0.2], - [500000, 0.18], - [1000000, 0.16], - [2500000, 0.14], - [5000000, 0.12], - [10000000, 0.1] - ], - "fees_maker": [ - [0, 0.16], - [50000, 0.14], - [100000, 0.12], - [250000, 0.1], - [500000, 0.08], - [1000000, 0.06], - [2500000, 0.04], - [5000000, 0.02], - [10000000, 0] - ], - "fee_volume_currency": "ZUSD", - "margin_call": 80, - "margin_stop": 40, + "error": [], + "result": { + "XBTUSDT": { + "altname": "XBTUSDT", + "wsname": "XBT/USDT", + "aclass_base": "currency", + "base": "XXBT", + "aclass_quote": "currency", + "quote": "USDT", + "lot": "unit", + "leverage_buy": [2, 3], + "leverage_sell": [2, 3], + "fees": [ + [0, 0.26], + [50000, 0.24], + [100000, 0.22], + [250000, 0.2], + [500000, 0.18], + [1000000, 0.16], + [2500000, 0.14], + [5000000, 0.12], + [10000000, 0.1] + ], + "fees_maker": [ + [0, 0.16], + [50000, 0.14], + [100000, 0.12], + [250000, 0.1], + [500000, 0.08], + [1000000, 0.06], + [2500000, 0.04], + [5000000, 0.02], + [10000000, 0] + ], + "fee_volume_currency": "ZUSD", + "margin_call": 80, + "margin_stop": 40, + } } } @property def order_creation_request_successful_mock_response(self): return { - "descr": { - "order": "", - }, - "txid": [ - self.expected_exchange_order_id, - ] + "error": [], + "result": { + "descr": { + "order": "", + }, + "txid": [ + self.expected_exchange_order_id, + ] + } } @property def balance_request_mock_response_for_base_and_quote(self): return { - self.base_asset: str(10), - self.quote_asset: str(2000), + "error": [], + "result": { + self.base_asset: str(10), + self.quote_asset: str(2000), + } } @property @@ -370,7 +429,7 @@ def expected_supported_order_types(self): @property def expected_trading_rule(self): - rule = list(self.trading_rules_request_mock_response.values())[0] + rule = list(self.trading_rules_request_mock_response["result"].values())[0] min_order_size = Decimal(rule.get('ordermin', 0)) min_price_increment = Decimal(f"1e-{rule.get('pair_decimals')}") min_base_amount_increment = Decimal(f"1e-{rule.get('lot_decimals')}") @@ -383,7 +442,7 @@ def expected_trading_rule(self): @property def expected_logged_error_for_erroneous_trading_rule(self): - erroneous_rule = list(self.trading_rules_request_erroneous_mock_response.values())[0] + erroneous_rule = list(self.trading_rules_request_erroneous_mock_response["result"].values())[0] return f"Error parsing the trading pair rule {erroneous_rule}. Skipping." @property @@ -447,7 +506,7 @@ def validate_order_cancelation_request(self, order: InFlightOrder, request_call: self.assertEqual(order.exchange_order_id, request_data["txid"]) def validate_order_status_request(self, order: InFlightOrder, request_call: RequestCall): - request_params = request_call.kwargs["params"] + request_params = request_call.kwargs["data"] self.assertEqual(order.exchange_order_id, request_params["txid"]) def validate_trades_request(self, order: InFlightOrder, request_call: RequestCall): @@ -770,6 +829,15 @@ def test_update_time_synchronizer_raises_cancelled_error(self, mock_api): def test_update_order_status_when_order_has_not_changed_and_one_partial_fill(self, mock_api): pass + @aioresponses() + def test_check_network_failure(self, mock_api): + url = self.network_status_url + mock_api.get(url, status=600) + + ret = self.async_run_with_timeout(coroutine=self.exchange.check_network()) + + self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) + @aioresponses() def test_update_order_status_when_failed(self, mock_api): self.exchange._set_current_timestamp(1640780000) @@ -813,14 +881,17 @@ def test_update_order_status_when_failed(self, mock_api): } } - mock_response = order_status + mock_response = { + "error": [], + "result": order_status + } mock_api.post(regex_url, body=json.dumps(mock_response)) self.async_run_with_timeout(self.exchange._update_order_status()) request = self._all_executed_requests(mock_api, url)[0] self.validate_auth_credentials_present(request) - request_params = request.kwargs["params"] + request_params = request.kwargs["data"] self.assertEqual(order.exchange_order_id, request_params["txid"]) failure_event: MarketOrderFailureEvent = self.order_failure_logger.event_log[0] @@ -989,13 +1060,20 @@ def get_asset_pairs_mock(self) -> Dict: "ordermin": "0.005" }, } - return asset_pairs + result = { + "error": [], + "result": asset_pairs + } + return result def get_balances_mock(self, base_asset_balance: float, quote_asset_balance: float) -> Dict: balances = { - self.base_asset: str(base_asset_balance), - self.quote_asset: str(quote_asset_balance), - "USDT": "171288.6158", + "error": [], + "result": { + self.base_asset: str(base_asset_balance), + self.quote_asset: str(quote_asset_balance), + "USDT": "171288.6158", + } } return balances @@ -1005,7 +1083,11 @@ def get_open_orders_mock(self, quantity: float, price: float, order_type: str) - "OQCLML-BW3P3-BUCMWZ": self.get_order_status_mock(quantity, price, order_type, status="open"), } } - return open_orders + result = { + "error": [], + "result": open_orders + } + return result def get_order_status_mock(self, quantity: float, price: float, order_type: str, status: str) -> Dict: order_status = { @@ -1062,52 +1144,61 @@ def test_update_balances(self, mocked_api): def _order_cancelation_request_successful_mock_response(self, order: InFlightOrder) -> Any: return { - "count": 1 + "error": [], + "result": { + "count": 1 + } } def _order_status_request_completely_filled_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "closed", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": str(order.amount), - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] + "error": [], + "result": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "closed", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": str(order.amount), + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } } } def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> Any: return { - order.exchange_order_id: { - "refid": "None", - "userref": 0, - "status": "canceled", - "opentm": 1688666559.8974, - "starttm": 0, - "expiretm": 0, - "descr": {}, - "vol": str(order.amount), - "vol_exec": "0", - "cost": "11253.7", - "fee": "0.00000", - "price": str(order.price), - "stopprice": "0.00000", - "limitprice": "0.00000", - "misc": "", - "oflags": "fciq", - "trades": [] + "error": [], + "result": { + order.exchange_order_id: { + "refid": "None", + "userref": 0, + "status": "canceled", + "opentm": 1688666559.8974, + "starttm": 0, + "expiretm": 0, + "descr": {}, + "vol": str(order.amount), + "vol_exec": "0", + "cost": "11253.7", + "fee": "0.00000", + "price": str(order.price), + "stopprice": "0.00000", + "limitprice": "0.00000", + "misc": "", + "oflags": "fciq", + "trades": [] + } } } @@ -1179,20 +1270,23 @@ def _order_fills_request_partial_fill_mock_response(self, order: InFlightOrder): def _order_fills_request_full_fill_mock_response(self, order: InFlightOrder): return { - self.expected_fill_trade_id: { - "ordertxid": order.exchange_order_id, - "postxid": "TKH2SE-M7IF5-CFI7LT", - "pair": "XXBTZUSD", - "time": 1499865549.590, - "type": "buy", - "ordertype": "limit", - "price": str(order.price), - "cost": "600.20000", - "fee": str(self.expected_fill_fee.flat_fees[0].amount), - "vol": str(order.amount), - "margin": "0.00000", - "misc": "", - "trade_id": 93748276, - "maker": "true" + "error": [], + "result": { + self.expected_fill_trade_id: { + "ordertxid": order.exchange_order_id, + "postxid": "TKH2SE-M7IF5-CFI7LT", + "pair": "XXBTZUSD", + "time": 1499865549.590, + "type": "buy", + "ordertype": "limit", + "price": str(order.price), + "cost": "600.20000", + "fee": str(self.expected_fill_fee.flat_fees[0].amount), + "vol": str(order.amount), + "margin": "0.00000", + "misc": "", + "trade_id": 93748276, + "maker": "true" + } } } From 48c1e8d33963a0cf3e17a48c87b588f95f38531e Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 29 Feb 2024 23:03:08 +0800 Subject: [PATCH 30/34] fix market price --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index 50bbf14185..bef4f6fbbd 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -358,6 +358,9 @@ async def _place_order(self, "userref": userref, "price": str(price) } + + if order_type is OrderType.MARKET: + del data["price"] if order_type is OrderType.LIMIT_MAKER: data["oflags"] = "post" order_result = await self._api_request_with_retry(RESTMethod.POST, From 6f8fc49f7aae986084895838f9b50428dcb7c914 Mon Sep 17 00:00:00 2001 From: bczhang Date: Sat, 2 Mar 2024 21:45:38 +0800 Subject: [PATCH 31/34] delete dirty code --- .../perp_cross_exchange_market_making.zip | Bin 24394 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 hummingbot/strategy/perp_cross_exchange_market_making.zip diff --git a/hummingbot/strategy/perp_cross_exchange_market_making.zip b/hummingbot/strategy/perp_cross_exchange_market_making.zip deleted file mode 100644 index fb9c3be87b6242d4a9d635c9a876880b0b4686b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24394 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 Date: Mon, 4 Mar 2024 14:09:53 +0800 Subject: [PATCH 32/34] modify Makefile --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index fd28c3db5f..9b592bd417 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,6 @@ test: --exclude-dir="test/mock" \ --exclude-dir="test/hummingbot/connector/gateway/amm" \ --exclude-dir="test/hummingbot/connector/exchange/coinbase_pro" \ - --exclude-dir="test/hummingbot/connector/exchange/kraken" \ --exclude-dir="test/hummingbot/connector/exchange/hitbtc" \ --exclude-dir="test/hummingbot/connector/gateway/clob_spot/data_sources/dexalot" \ --exclude-dir="test/hummingbot/strategy/amm_arb" \ From fe1bc6596cc204a2f31fd27981905d12fb2ef4a5 Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 7 Mar 2024 20:11:33 +0800 Subject: [PATCH 33/34] delete krakenInFigheOrder --- .../exchange/kraken/kraken_constants.py | 2 + .../exchange/kraken/kraken_exchange.py | 105 +++----------- .../exchange/kraken/kraken_in_fight_order.py | 131 ------------------ .../exchange/kraken/test_kraken_exchange.py | 106 +++----------- .../kraken/test_kraken_in_flight_order.py | 92 ------------ 5 files changed, 37 insertions(+), 399 deletions(-) delete mode 100644 hummingbot/connector/exchange/kraken/kraken_in_fight_order.py delete mode 100644 test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py diff --git a/hummingbot/connector/exchange/kraken/kraken_constants.py b/hummingbot/connector/exchange/kraken/kraken_constants.py index 6e895cf59d..d8c00ffbc8 100644 --- a/hummingbot/connector/exchange/kraken/kraken_constants.py +++ b/hummingbot/connector/exchange/kraken/kraken_constants.py @@ -8,6 +8,8 @@ MAX_ORDER_ID_LEN = 32 HBOT_ORDER_ID_PREFIX = "HBOT" +MAX_ID_BIT_COUNT = 31 + class KrakenAPITier(Enum): """ diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index bef4f6fbbd..fa48505cb2 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -12,7 +12,6 @@ from hummingbot.connector.exchange.kraken.kraken_api_user_stream_data_source import KrakenAPIUserStreamDataSource from hummingbot.connector.exchange.kraken.kraken_auth import KrakenAuth from hummingbot.connector.exchange.kraken.kraken_constants import KrakenAPITier -from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder from hummingbot.connector.exchange.kraken.kraken_utils import ( build_rate_limits_by_tier, convert_from_exchange_symbol, @@ -20,7 +19,7 @@ ) 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.connector.utils import get_new_numeric_client_order_id from hummingbot.core.api_throttler.async_throttler import AsyncThrottler from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderUpdate, TradeUpdate @@ -29,6 +28,7 @@ from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.core.utils.estimate_fee import build_trade_fee +from hummingbot.core.utils.tracking_nonce import NonceCreator from hummingbot.core.web_assistant.connections.data_types import RESTMethod from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory @@ -59,8 +59,8 @@ def __init__(self, self._trading_pairs = trading_pairs self._kraken_api_tier = KrakenAPITier(kraken_api_tier.upper()) self._asset_pairs = {} - self._last_userref = 0 self._client_config = client_config_map + self._client_order_id_nonce_provider = NonceCreator.for_microseconds() self._throttler = self._build_async_throttler(api_tier=self._kraken_api_tier) super().__init__(client_config_map) @@ -201,10 +201,6 @@ async def _api_delete(self, *args, **kwargs): kwargs["method"] = RESTMethod.DELETE return await self._api_request_with_retry(*args, **kwargs) - def generate_userref(self): - self._last_userref += 1 - return self._last_userref - @staticmethod def is_cloudflare_exception(exception: Exception): """ @@ -238,21 +234,17 @@ def buy(self, :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 - ) - userref = self.generate_userref() + order_id = str(get_new_numeric_client_order_id( + nonce_creator=self._client_order_id_nonce_provider, + max_id_bit_count=CONSTANTS.MAX_ID_BIT_COUNT, + )) safe_ensure_future(self._create_order( trade_type=TradeType.BUY, order_id=order_id, trading_pair=trading_pair, amount=amount, order_type=order_type, - price=price, - userref=userref)) + price=price)) return order_id def sell(self, @@ -269,21 +261,17 @@ def sell(self, :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 - ) - userref = self.generate_userref() + order_id = str(get_new_numeric_client_order_id( + nonce_creator=self._client_order_id_nonce_provider, + max_id_bit_count=CONSTANTS.MAX_ID_BIT_COUNT, + )) safe_ensure_future(self._create_order( trade_type=TradeType.SELL, order_id=order_id, trading_pair=trading_pair, amount=amount, order_type=order_type, - price=price, - userref=userref)) + price=price)) return order_id async def get_asset_pairs(self) -> Dict[str, Any]: @@ -295,50 +283,6 @@ async def get_asset_pairs(self) -> Dict[str, Any]: web_utils.is_exchange_information_valid(details)} return self._asset_pairs - def start_tracking_order(self, - order_id: str, - exchange_order_id: Optional[str], - trading_pair: str, - trade_type: TradeType, - price: Decimal, - amount: Decimal, - order_type: OrderType, - **kwargs): - """ - Starts tracking an order by adding it to the order tracker. - - :param order_id: the order identifier - :param exchange_order_id: the identifier for the order in the exchange - :param trading_pair: the token pair for the operation - :param trade_type: the type of order (buy or sell) - :param price: the price for the order - :param amount: the amount for the order - :param order_type: type of execution for the order (MARKET, LIMIT, LIMIT_MAKER) - """ - userref = kwargs.get("userref", 0) - self._order_tracker.start_tracking_order( - KrakenInFlightOrder( - client_order_id=order_id, - exchange_order_id=exchange_order_id, - trading_pair=trading_pair, - order_type=order_type, - trade_type=trade_type, - amount=amount, - price=price, - creation_timestamp=self.current_timestamp, - userref=userref, - ) - ) - - def restore_tracking_states(self, saved_states: Dict[str, Any]): - for serialized_order in saved_states.values(): - order = KrakenInFlightOrder.from_json(serialized_order) - if order.is_open: - self._order_tracker._in_flight_orders[order.client_order_id] = order - elif order.is_failure: - # If the order is marked as failed but is still in the tracking states, it was a lost order - self._order_tracker._lost_orders[order.client_order_id] = order - self._last_userref = max(int(order.userref), self._last_userref) async def _place_order(self, order_id: str, @@ -348,14 +292,13 @@ async def _place_order(self, order_type: OrderType, price: Decimal, **kwargs) -> Tuple[str, float]: - userref = kwargs.get("userref", 0) trading_pair = await self.exchange_symbol_associated_to_pair(trading_pair=trading_pair) data = { "pair": trading_pair, "type": "buy" if trade_type is TradeType.BUY else "sell", "ordertype": "market" if order_type is OrderType.MARKET else "limit", "volume": str(amount), - "userref": userref, + "userref": order_id, "price": str(price) } @@ -560,22 +503,11 @@ def _process_trade_message(self, trades: List): trade: Dict[str, str] = update[trade_id] trade["trade_id"] = trade_id exchange_order_id = trade.get("ordertxid") - _userref = trade.get("userref") - tracked_order = self._order_tracker.all_fillable_orders_by_exchange_order_id.get(exchange_order_id) + client_order_id = str(trade.get("userref","")) + tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) if not tracked_order: - all_orders = self._order_tracker.all_fillable_orders - for k, v in all_orders.items(): - if v.userref == _userref: - tracked_order = v - break - if not tracked_order: - self.logger().debug(f"Ignoring trade message with id {exchange_order_id}: not in in_flight_orders.") - else: - trade_update = self._create_trade_update_with_order_fill_data( - order_fill=trade, - order=tracked_order) - self._order_tracker.process_trade_update(trade_update) + self.logger().debug(f"Ignoring trade message with id {exchange_order_id}: not in in_flight_orders.") else: trade_update = self._create_trade_update_with_order_fill_data( order_fill=trade, @@ -596,7 +528,8 @@ def _process_order_message(self, orders: List): update = orders[0] for message in update: for exchange_order_id, order_msg in message.items(): - tracked_order = self._order_tracker.all_updatable_orders_by_exchange_order_id.get(exchange_order_id) + client_order_id = str(order_msg.get("userref","")) + tracked_order = self._order_tracker.all_updatable_orders.get(client_order_id) if not tracked_order: self.logger().debug( f"Ignoring order message with id {order_msg}: not in in_flight_orders.") diff --git a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py b/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py deleted file mode 100644 index fd859e94b7..0000000000 --- a/hummingbot/connector/exchange/kraken/kraken_in_fight_order.py +++ /dev/null @@ -1,131 +0,0 @@ -import copy -import math -from decimal import Decimal -from typing import Any, Dict, Optional, Tuple - -from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType -from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState, TradeUpdate - - -class KrakenInFlightOrder(InFlightOrder): - def __init__( - self, - client_order_id: str, - trading_pair: str, - order_type: OrderType, - trade_type: TradeType, - amount: Decimal, - creation_timestamp: float, - userref: int, - price: Optional[Decimal] = None, - exchange_order_id: Optional[str] = None, - initial_state: OrderState = OrderState.PENDING_CREATE, - leverage: int = 1, - position: PositionAction = PositionAction.NIL, - - ) -> None: - super().__init__( - client_order_id, - trading_pair, - order_type, - trade_type, - amount, - creation_timestamp, - price, - exchange_order_id, - initial_state, - leverage, - position, - ) - self.userref = userref - - @property - def is_done(self) -> bool: - return (self.current_state in {OrderState.CANCELED, OrderState.FILLED, OrderState.FAILED} - or math.isclose(self.executed_amount_base, self.amount) - or self.executed_amount_base >= self.amount - ) - - @property - def attributes(self) -> Tuple[Any]: - return copy.deepcopy( - ( - self.client_order_id, - self.trading_pair, - self.order_type, - self.trade_type, - self.price, - self.amount, - self.exchange_order_id, - self.current_state, - self.leverage, - self.position, - self.userref, - self.executed_amount_base, - self.executed_amount_quote, - self.creation_timestamp, - self.last_update_timestamp, - ) - ) - - @classmethod - def from_json(cls, data: Dict[str, Any]) -> "InFlightOrder": - """ - Initialize an InFlightOrder using a JSON object - :param data: JSON data - :return: Formatted InFlightOrder - """ - order = KrakenInFlightOrder( - client_order_id=data["client_order_id"], - trading_pair=data["trading_pair"], - order_type=getattr(OrderType, data["order_type"]), - trade_type=getattr(TradeType, data["trade_type"]), - amount=Decimal(data["amount"]), - price=Decimal(data["price"]), - exchange_order_id=data["exchange_order_id"], - initial_state=OrderState(int(data["last_state"])), - leverage=int(data["leverage"]), - position=PositionAction(data["position"]), - creation_timestamp=data.get("creation_timestamp", -1), - userref=data.get("userref", 0) - ) - order.executed_amount_base = Decimal(data["executed_amount_base"]) - order.executed_amount_quote = Decimal(data["executed_amount_quote"]) - order.order_fills.update({key: TradeUpdate.from_json(value) - for key, value - in data.get("order_fills", {}).items()}) - order.last_update_timestamp = data.get("last_update_timestamp", order.creation_timestamp) - - order.check_filled_condition() - order.check_processed_by_exchange_condition() - - return order - - def to_json(self) -> Dict[str, Any]: - """ - Returns this InFlightOrder as a JSON object. - :return: JSON object - """ - return { - "client_order_id": self.client_order_id, - "exchange_order_id": self.exchange_order_id, - "trading_pair": self.trading_pair, - "order_type": self.order_type.name, - "trade_type": self.trade_type.name, - "price": str(self.price), - "amount": str(self.amount), - "executed_amount_base": str(self.executed_amount_base), - "executed_amount_quote": str(self.executed_amount_quote), - "last_state": str(self.current_state.value), - "leverage": str(self.leverage), - "position": self.position.value, - "userref": self.userref, - "creation_timestamp": self.creation_timestamp, - "last_update_timestamp": self.last_update_timestamp, - "order_fills": {key: fill.to_json() for key, fill in self.order_fills.items()} - } - - def check_filled_condition(self): - if (abs(self.amount) - self.executed_amount_base).quantize(Decimal('1e-8')) <= 0 \ - or self.current_state == OrderState.FILLED: - self.completely_filled_event.set() diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index c3c19175ff..627fc4e3ff 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -12,15 +12,15 @@ from hummingbot.client.config.config_helpers import ClientConfigAdapter from hummingbot.connector.exchange.kraken import kraken_constants as CONSTANTS, kraken_web_utils as web_utils from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange -from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder from hummingbot.connector.test_support.exchange_connector_test import AbstractExchangeConnectorTests from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import get_new_client_order_id +from hummingbot.connector.utils import get_new_numeric_client_order_id from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.event.events import MarketOrderFailureEvent from hummingbot.core.network_iterator import NetworkStatus +from hummingbot.core.utils.tracking_nonce import NonceCreator class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests): @@ -37,7 +37,6 @@ def setUpClass(cls) -> None: cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = cls.ex_base_asset + cls.quote_asset cls.ws_ex_trading_pairs = cls.ex_base_asset + "/" + cls.quote_asset - cls._userref = 0 @property def all_symbols_url(self): @@ -682,7 +681,7 @@ def order_event_for_new_order_websocket_update(self, order: InFlightOrder): "starttm": "0.000000", "status": "open", "stopprice": "0.000000", - "userref": 0, + "userref": order.client_order_id, "vol": str(order.amount, ), "vol_exec": "0.00000000" } @@ -721,7 +720,7 @@ def order_event_for_canceled_order_websocket_update(self, order: InFlightOrder): "starttm": "0.000000", "status": "canceled", "stopprice": "0.000000", - "userref": 0, + "userref": order.client_order_id, "vol": "10.00345345", "vol_exec": "0.00000000" } @@ -760,7 +759,7 @@ def order_event_for_full_fill_websocket_update(self, order: InFlightOrder): "starttm": "0.000000", "status": "closed", "stopprice": "0.000000", - "userref": 0, + "userref": order.client_order_id, "vol": order.amount, "vol_exec": "0.00000000" } @@ -787,6 +786,7 @@ def trade_event_for_full_fill_websocket_update(self, order: InFlightOrder): "price": str(order.price), "time": "1560516023.070651", "type": "sell", + "userref": order.client_order_id, "vol": str(order.amount) } } @@ -852,7 +852,6 @@ def test_update_order_status_when_failed(self, mock_api): trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), - userref=0, ) order = self.exchange.in_flight_orders["OID1"] @@ -862,7 +861,7 @@ def test_update_order_status_when_failed(self, mock_api): order_status = { order.exchange_order_id: { "refid": "None", - "userref": 0, + "userref": order.client_order_id, "status": "expired", "opentm": 1499827319.559, "starttm": 0, @@ -908,22 +907,16 @@ def test_update_order_status_when_failed(self, mock_api): "misc_updates=None)") ) - @patch("hummingbot.connector.utils.get_tracking_nonce") - def test_client_order_id_on_order(self, mocked_nonce): - mocked_nonce.return_value = 7 - + @patch("hummingbot.connector.exchange.kraken.kraken_exchange.get_new_numeric_client_order_id") + def test_client_order_id_on_order(self, mock_ts): + mock_ts.return_value = 7 result = self.exchange.buy( trading_pair=self.trading_pair, amount=Decimal("1"), order_type=OrderType.LIMIT, price=Decimal("2"), ) - expected_client_order_id = get_new_client_order_id( - is_buy=True, - trading_pair=self.trading_pair, - hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID_PREFIX, - max_id_len=CONSTANTS.MAX_ORDER_ID_LEN, - ) + expected_client_order_id = str(7) self.assertEqual(result, expected_client_order_id) @@ -933,12 +926,7 @@ def test_client_order_id_on_order(self, mocked_nonce): order_type=OrderType.LIMIT, price=Decimal("2"), ) - expected_client_order_id = get_new_client_order_id( - is_buy=False, - trading_pair=self.trading_pair, - hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID_PREFIX, - max_id_len=CONSTANTS.MAX_ORDER_ID_LEN, - ) + expected_client_order_id = str(7) self.assertEqual(result, expected_client_order_id) @@ -951,68 +939,6 @@ def _validate_auth_credentials_taking_parameters_from_argument(self, self.assertIn("API-Key", request_headers) self.assertEqual("someKey", request_headers["API-Key"]) - def test_restore_tracking_states_only_registers_open_orders(self): - orders = [] - orders.append(KrakenInFlightOrder( - client_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, - amount=Decimal("1000.0"), - price=Decimal("1.0"), - creation_timestamp=1640001112.223, - userref=self._userref - )) - orders.append(KrakenInFlightOrder( - client_order_id=self.client_order_id_prefix + "2", - exchange_order_id=self.exchange_order_id_prefix + "2", - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - amount=Decimal("1000.0"), - price=Decimal("1.0"), - creation_timestamp=1640001112.223, - initial_state=OrderState.CANCELED, - userref=self._userref - - )) - orders.append(KrakenInFlightOrder( - client_order_id=self.client_order_id_prefix + "3", - exchange_order_id=self.exchange_order_id_prefix + "3", - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - amount=Decimal("1000.0"), - price=Decimal("1.0"), - creation_timestamp=1640001112.223, - initial_state=OrderState.FILLED, - userref=self._userref - - )) - orders.append(KrakenInFlightOrder( - client_order_id=self.client_order_id_prefix + "4", - exchange_order_id=self.exchange_order_id_prefix + "4", - trading_pair=self.trading_pair, - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - amount=Decimal("1000.0"), - price=Decimal("1.0"), - creation_timestamp=1640001112.223, - initial_state=OrderState.FAILED, - userref=self._userref - - )) - - tracking_states = {order.client_order_id: order.to_json() for order in orders} - - self.exchange.restore_tracking_states(tracking_states) - - self.assertIn(self.client_order_id_prefix + "1", self.exchange.in_flight_orders) - self.assertNotIn(self.client_order_id_prefix + "2", self.exchange.in_flight_orders) - self.assertNotIn(self.client_order_id_prefix + "3", self.exchange.in_flight_orders) - self.assertNotIn(self.client_order_id_prefix + "4", self.exchange.in_flight_orders) - def get_asset_pairs_mock(self) -> Dict: asset_pairs = { f"X{self.base_asset}{self.quote_asset}": { @@ -1156,7 +1082,7 @@ def _order_status_request_completely_filled_mock_response(self, order: InFlightO "result": { order.exchange_order_id: { "refid": "None", - "userref": 0, + "userref": order.client_order_id, "status": "closed", "opentm": 1688666559.8974, "starttm": 0, @@ -1182,7 +1108,7 @@ def _order_status_request_canceled_mock_response(self, order: InFlightOrder) -> "result": { order.exchange_order_id: { "refid": "None", - "userref": 0, + "userref": order.client_order_id, "status": "canceled", "opentm": 1688666559.8974, "starttm": 0, @@ -1206,7 +1132,7 @@ def _order_status_request_open_mock_response(self, order: InFlightOrder) -> Any: return { order.exchange_order_id: { "refid": "None", - "userref": 0, + "userref": order.client_order_id, "status": "open", "opentm": 1688666559.8974, "starttm": 0, @@ -1229,7 +1155,7 @@ def _order_status_request_partially_filled_mock_response(self, order: InFlightOr return { order.exchange_order_id: { "refid": "None", - "userref": 0, + "userref": order.client_order_id, "status": "open", "opentm": 1688666559.8974, "starttm": 0, diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py b/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py deleted file mode 100644 index 35c953c0ca..0000000000 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_in_flight_order.py +++ /dev/null @@ -1,92 +0,0 @@ -from decimal import Decimal -from unittest import TestCase - -from hummingbot.connector.exchange.kraken.kraken_in_fight_order import KrakenInFlightOrder -from hummingbot.core.data_type.common import OrderType, PositionAction, TradeType -from hummingbot.core.data_type.in_flight_order import OrderState - - -class KrakenInFlightOrderTests(TestCase): - def test_order_is_local_after_creation(self): - order = KrakenInFlightOrder( - client_order_id="someId", - exchange_order_id=None, - trading_pair="BTC-USDT", - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal(45000), - amount=Decimal(1), - creation_timestamp=1640001112.0, - userref=1, - ) - - self.assertTrue(order.is_pending_create) - - def test_serialize_order_to_json(self): - order = KrakenInFlightOrder( - client_order_id="OID1", - exchange_order_id="EOID1", - trading_pair="COINALPHA-HBOT", - order_type=OrderType.LIMIT, - trade_type=TradeType.BUY, - price=Decimal(1000), - amount=Decimal(1), - creation_timestamp=1640001112.0, - userref=2, - initial_state=OrderState.OPEN, - ) - - expected_json = { - "client_order_id": order.client_order_id, - "exchange_order_id": order.exchange_order_id, - "trading_pair": order.trading_pair, - "order_type": order.order_type.name, - "trade_type": order.trade_type.name, - "price": str(order.price), - "amount": str(order.amount), - "executed_amount_base": str(order.executed_amount_base), - "executed_amount_quote": str(order.executed_amount_quote), - "last_state": str(OrderState.OPEN.value), - "leverage": "1", - "position": PositionAction.NIL.value, - "userref": 2, - "creation_timestamp": 1640001112.0, - "last_update_timestamp": 1640001112.0, - "order_fills": {} - } - - self.assertEqual(expected_json, order.to_json()) - - def test_deserialize_order_from_json(self): - json = { - "client_order_id": "OID1", - "exchange_order_id": "EOID1", - "trading_pair": "COINALPHA-HBOT", - "order_type": OrderType.LIMIT.name, - "trade_type": TradeType.BUY.name, - "price": "1000", - "amount": "1", - "last_state": "1", - "leverage": "1", - "position": PositionAction.NIL.value, - "executed_amount_base": "0.1", - "executed_amount_quote": "110", - "fee_asset": "BNB", - "fee_paid": "10", - "creation_timestamp": 1640001112.0, - "userref": 2, - } - - order: KrakenInFlightOrder = KrakenInFlightOrder.from_json(json) - - self.assertEqual(json["client_order_id"], order.client_order_id) - self.assertEqual(json["exchange_order_id"], order.exchange_order_id) - self.assertEqual(json["trading_pair"], order.trading_pair) - self.assertEqual(OrderType.LIMIT, order.order_type) - self.assertEqual(TradeType.BUY, order.trade_type) - self.assertEqual(Decimal(json["price"]), order.price) - self.assertEqual(Decimal(json["amount"]), order.amount) - self.assertEqual(Decimal(json["executed_amount_base"]), order.executed_amount_base) - self.assertEqual(Decimal(json["executed_amount_quote"]), order.executed_amount_quote) - self.assertEqual(json["creation_timestamp"], order.creation_timestamp) - self.assertEqual(json["userref"], order.userref) From 643da260e5fe87add9b47c2872a1ff1188eae4a2 Mon Sep 17 00:00:00 2001 From: bczhang Date: Thu, 7 Mar 2024 21:33:34 +0800 Subject: [PATCH 34/34] delete krakenInFigheOrder --- hummingbot/connector/exchange/kraken/kraken_exchange.py | 5 ++--- .../connector/exchange/kraken/test_kraken_exchange.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/hummingbot/connector/exchange/kraken/kraken_exchange.py b/hummingbot/connector/exchange/kraken/kraken_exchange.py index fa48505cb2..2888e88edc 100644 --- a/hummingbot/connector/exchange/kraken/kraken_exchange.py +++ b/hummingbot/connector/exchange/kraken/kraken_exchange.py @@ -283,7 +283,6 @@ async def get_asset_pairs(self) -> Dict[str, Any]: web_utils.is_exchange_information_valid(details)} return self._asset_pairs - async def _place_order(self, order_id: str, trading_pair: str, @@ -503,7 +502,7 @@ def _process_trade_message(self, trades: List): trade: Dict[str, str] = update[trade_id] trade["trade_id"] = trade_id exchange_order_id = trade.get("ordertxid") - client_order_id = str(trade.get("userref","")) + client_order_id = str(trade.get("userref", "")) tracked_order = self._order_tracker.all_fillable_orders.get(client_order_id) if not tracked_order: @@ -528,7 +527,7 @@ def _process_order_message(self, orders: List): update = orders[0] for message in update: for exchange_order_id, order_msg in message.items(): - client_order_id = str(order_msg.get("userref","")) + client_order_id = str(order_msg.get("userref", "")) tracked_order = self._order_tracker.all_updatable_orders.get(client_order_id) if not tracked_order: self.logger().debug( diff --git a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py index 627fc4e3ff..0175602391 100644 --- a/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py +++ b/test/hummingbot/connector/exchange/kraken/test_kraken_exchange.py @@ -14,13 +14,11 @@ from hummingbot.connector.exchange.kraken.kraken_exchange import KrakenExchange from hummingbot.connector.test_support.exchange_connector_test import AbstractExchangeConnectorTests from hummingbot.connector.trading_rule import TradingRule -from hummingbot.connector.utils import get_new_numeric_client_order_id from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.core.data_type.in_flight_order import InFlightOrder, OrderState from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TokenAmount, TradeFeeBase from hummingbot.core.event.events import MarketOrderFailureEvent from hummingbot.core.network_iterator import NetworkStatus -from hummingbot.core.utils.tracking_nonce import NonceCreator class KrakenExchangeTests(AbstractExchangeConnectorTests.ExchangeConnectorTests):