diff --git a/hummingbot/connector/exchange/beaxy/__init__.py b/hummingbot/connector/exchange/beaxy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pxd b/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pxd new file mode 100644 index 0000000000..d933b5f43d --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pxd @@ -0,0 +1,9 @@ +# distutils: language=c++ +cimport numpy as np + +cdef class BeaxyActiveOrderTracker: + cdef dict _active_bids + cdef dict _active_asks + + cdef tuple c_convert_diff_message_to_np_arrays(self, object message) + cdef tuple c_convert_snapshot_message_to_np_arrays(self, object message) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pyx b/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pyx new file mode 100644 index 0000000000..ce9e95a98f --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_active_order_tracker.pyx @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- + +# distutils: language=c++ +# distutils: sources=hummingbot/core/cpp/OrderBookEntry.cpp + +import logging +import numpy as np +from decimal import Decimal +from typing import Dict + +from hummingbot.logger import HummingbotLogger +from hummingbot.core.data_type.order_book_row import OrderBookRow + +_bxaot_logger = None +s_empty_diff = np.ndarray(shape=(0, 4), dtype='float64') + +BeaxyOrderBookTrackingDictionary = Dict[Decimal, Decimal] + +ACTION_UPDATE = 'UPDATE' +ACTION_INSERT = 'INSERT' +ACTION_DELETE = 'DELETE' +ACTION_DELETE_THROUGH = 'DELETE_THROUGH' +ACTION_DELETE_FROM = 'DELETE_FROM' +SIDE_BID = 'BID' +SIDE_ASK = 'ASK' + +cdef class BeaxyActiveOrderTracker: + def __init__(self, + active_asks: BeaxyOrderBookTrackingDictionary = None, + active_bids: BeaxyOrderBookTrackingDictionary = None): + super().__init__() + self._active_asks = active_asks or {} + self._active_bids = active_bids or {} + + @classmethod + def logger(cls) -> HummingbotLogger: + global _bxaot_logger + if _bxaot_logger is None: + _bxaot_logger = logging.getLogger(__name__) + return _bxaot_logger + + @property + def active_asks(self) -> BeaxyOrderBookTrackingDictionary: + """ + Get all asks on the order book in dictionary format + :returns: Dict[price, Dict[order_id, order_book_message]] + """ + return self._active_asks + + @property + def active_bids(self) -> BeaxyOrderBookTrackingDictionary: + """ + Get all bids on the order book in dictionary format + :returns: Dict[price, Dict[order_id, order_book_message]] + """ + return self._active_bids + + def volume_for_ask_price(self, price) -> float: + return sum([float(msg['remaining_size']) for msg in self._active_asks[price].values()]) + + def volume_for_bid_price(self, price) -> float: + return sum([float(msg['remaining_size']) for msg in self._active_bids[price].values()]) + + def get_rates_and_quantities(self, entry) -> tuple: + return float(entry['rate']), float(entry['quantity']) + + def is_entry_valid(self, entry): + return all([k in entry for k in ['side', 'action', 'price']]) + + cdef tuple c_convert_diff_message_to_np_arrays(self, object message): + """ + Interpret an incoming diff message and apply changes to the order book accordingly + :returns: new order book rows: Tuple(np.array (bids), np.array (asks)) + """ + + def diff(side): + + for entry in message.content['entries']: + + if not self.is_entry_valid(entry): + continue + + if entry['side'] != side: + continue + + msg_action = entry['action'] + order_side = entry['side'] + timestamp = message.timestamp + + price = Decimal(str(entry['price'])) + + active_rows = self._active_bids if order_side == SIDE_BID else self._active_asks + + if msg_action in (ACTION_UPDATE, ACTION_INSERT): + + if 'quantity' not in entry: + continue + + quantity = Decimal(str(entry['quantity'])) + + active_rows[price] = quantity + yield [timestamp, float(price), quantity, message.update_id] + + elif msg_action == ACTION_DELETE: + + if price not in active_rows: + continue + + del active_rows[price] + yield [timestamp, float(price), float(0), message.update_id] + + elif msg_action == ACTION_DELETE_THROUGH: + # Remove all levels from the specified and below (all the worst prices). + for key in list(active_rows.keys()): + if key < price: + del active_rows[key] + yield [timestamp, float(price), float(0), message.update_id] + + elif msg_action == ACTION_DELETE_FROM: + # Remove all levels from the specified and above (all the better prices). + for key in list(active_rows.keys()): + if key > price: + del active_rows[key] + yield [timestamp, float(price), float(0), message.update_id] + + else: + continue + + bids = np.array([r for r in diff(SIDE_BID)], dtype='float64', ndmin=2) + asks = np.array([r for r in diff(SIDE_ASK)], dtype='float64', ndmin=2) + + # If there're no rows, the shape would become (1, 0) and not (0, 4). + # Reshape to fix that. + if bids.shape[1] != 4: + bids = bids.reshape((0, 4)) + if asks.shape[1] != 4: + asks = asks.reshape((0, 4)) + + return bids, asks + + cdef tuple c_convert_snapshot_message_to_np_arrays(self, object message): + """ + Interpret an incoming snapshot message and apply changes to the order book accordingly + :returns: new order book rows: Tuple(np.array (bids), np.array (asks)) + """ + + # Refresh all order tracking. + self._active_bids.clear() + self._active_asks.clear() + + for entry in message.content['entries']: + + if not self.is_entry_valid(entry): + continue + + quantity = Decimal(str(entry['quantity'])) + price = Decimal(str(entry['price'])) + side = entry['side'] + + if side == SIDE_ASK: + self.active_asks[price] = quantity + elif side == SIDE_BID: + self.active_bids[price] = quantity + else: + continue + + # Return the sorted snapshot tables. + cdef: + np.ndarray[np.float64_t, ndim=2] bids = np.array( + [[message.timestamp, + float(price), + float(self._active_bids[price]), + message.update_id] + for price in sorted(self._active_bids.keys(), reverse=True)], dtype='float64', ndmin=2) + np.ndarray[np.float64_t, ndim=2] asks = np.array( + [[message.timestamp, + float(price), + float(self._active_asks[price]), + message.update_id] + for price in sorted(self._active_asks.keys(), reverse=True)], dtype='float64', ndmin=2) + + # If there're no rows, the shape would become (1, 0) and not (0, 4). + # Reshape to fix that. + if bids.shape[1] != 4: + bids = bids.reshape((0, 4)) + if asks.shape[1] != 4: + asks = asks.reshape((0, 4)) + + return bids, asks + + def convert_diff_message_to_order_book_row(self, message): + """ + Convert an incoming diff message to Tuple of np.arrays, and then convert to OrderBookRow + :returns: Tuple(List[bids_row], List[asks_row]) + """ + np_bids, np_asks = self.c_convert_diff_message_to_np_arrays(message) + bids_row = [OrderBookRow(price, qty, update_id) for ts, price, qty, update_id in np_bids] + asks_row = [OrderBookRow(price, qty, update_id) for ts, price, qty, update_id in np_asks] + return bids_row, asks_row + + def convert_snapshot_message_to_order_book_row(self, message): + """ + Convert an incoming snapshot message to Tuple of np.arrays, and then convert to OrderBookRow + :returns: Tuple(List[bids_row], List[asks_row]) + """ + np_bids, np_asks = self.c_convert_snapshot_message_to_np_arrays(message) + bids_row = [OrderBookRow(price, qty, update_id) for ts, price, qty, update_id in np_bids] + asks_row = [OrderBookRow(price, qty, update_id) for ts, price, qty, update_id in np_asks] + return bids_row, asks_row diff --git a/hummingbot/connector/exchange/beaxy/beaxy_api_order_book_data_source.py b/hummingbot/connector/exchange/beaxy/beaxy_api_order_book_data_source.py new file mode 100644 index 0000000000..f42881c33f --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_api_order_book_data_source.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- + +import logging +import aiohttp +import asyncio +import json +import ujson +import cachetools.func +from typing import Any, AsyncIterable, Optional, List, Dict +from decimal import Decimal + +import pandas as pd +import websockets +import requests + +from websockets.exceptions import ConnectionClosed +from hummingbot.logger import HummingbotLogger +from hummingbot.core.utils import async_ttl_cache +from hummingbot.core.data_type.order_book_tracker_entry import OrderBookTrackerEntry +from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource +from hummingbot.core.data_type.order_book_message import OrderBookMessage +from hummingbot.core.data_type.order_book import OrderBook + +from hummingbot.connector.exchange.beaxy.beaxy_constants import BeaxyConstants +from hummingbot.connector.exchange.beaxy.beaxy_active_order_tracker import BeaxyActiveOrderTracker +from hummingbot.connector.exchange.beaxy.beaxy_order_book import BeaxyOrderBook +from hummingbot.connector.exchange.beaxy.beaxy_misc import split_market_pairs, trading_pair_to_symbol +from hummingbot.connector.exchange.beaxy.beaxy_order_book_tracker_entry import BeaxyOrderBookTrackerEntry + + +ORDERBOOK_MESSAGE_SNAPSHOT = 'SNAPSHOT_FULL_REFRESH' +ORDERBOOK_MESSAGE_DIFF = 'INCREMENTAL_UPDATE' + + +class BeaxyAPIOrderBookDataSource(OrderBookTrackerDataSource): + + MESSAGE_TIMEOUT = 30.0 + PING_TIMEOUT = 10.0 + _bxyaobds_logger: Optional[HummingbotLogger] = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls._bxyaobds_logger is None: + cls._bxyaobds_logger = logging.getLogger(__name__) + return cls._bxyaobds_logger + + def __init__(self, trading_pairs: List[str]): + super().__init__(trading_pairs) + + @classmethod + @async_ttl_cache(ttl=60 * 30, maxsize=1) + async def get_active_exchange_markets(cls) -> pd.DataFrame: + async with aiohttp.ClientSession() as client: + + symbols_response: aiohttp.ClientResponse = await client.get(BeaxyConstants.PublicApi.SYMBOLS_URL) + rates_response: aiohttp.ClientResponse = await client.get(BeaxyConstants.PublicApi.RATES_URL) + + if symbols_response.status != 200: + raise IOError(f'Error fetching Beaxy markets information. ' + f'HTTP status is {symbols_response.status}.') + if rates_response.status != 200: + raise IOError(f'Error fetching Beaxy exchange information. ' + f'HTTP status is {symbols_response.status}.') + + symbols_data = await symbols_response.json() + rates_data = await rates_response.json() + + market_data: List[Dict[str, Any]] = [{'pair': pair, **rates_data[pair], **item} + for pair in rates_data + for item in symbols_data + if item['suspendedForTrading'] is False + if pair in item['symbol']] + + all_markets: pd.DataFrame = pd.DataFrame.from_records(data=market_data, index='pair') + + btc_price: float = float(all_markets.loc['BTCUSDC'].price) + eth_price: float = float(all_markets.loc['ETHUSDC'].price) + + usd_volume: List[float] = [ + ( + volume * quote_price if trading_pair.endswith(('USDC')) else + volume * quote_price * btc_price if trading_pair.endswith('BTC') else + volume * quote_price * eth_price if trading_pair.endswith('ETH') else + volume + ) + for trading_pair, volume, quote_price in zip( + all_markets.index, + all_markets.volume24.astype('float'), + all_markets.price.astype('float') + ) + ] + + all_markets.loc[:, 'USDVolume'] = usd_volume + del all_markets['volume'] + all_markets.rename(columns={'baseCurrency': 'baseAsset', + 'termCurrency': 'quoteAsset', + 'volume24': 'volume'}, inplace=True) + + return all_markets.sort_values('USDVolume', ascending=False) + + @staticmethod + async def fetch_trading_pairs() -> Optional[List[str]]: + try: + async with aiohttp.ClientSession() as client: + async with client.get(BeaxyConstants.PublicApi.SYMBOLS_URL, timeout=5) as response: + if response.status == 200: + all_trading_pairs: List[Dict[str, Any]] = await response.json() + return ['{}-{}'.format(*p) for p in + split_market_pairs([i['symbol'] for i in all_trading_pairs])] + except Exception: # nopep8 + # Do nothing if the request fails -- there will be no autocomplete for beaxy trading pairs + pass + return [] + + @staticmethod + @cachetools.func.ttl_cache(ttl=10) + def get_mid_price(trading_pair: str) -> Optional[Decimal]: + + symbol = trading_pair_to_symbol(trading_pair) + resp = requests.get(url=BeaxyConstants.PublicApi.RATE_URL.format(symbol=symbol)) + record = resp.json() + + return (Decimal(record['bid']) + Decimal(record['ask'])) / Decimal('2') + + @classmethod + async def get_last_traded_prices(cls, trading_pairs: List[str]) -> Dict[str, float]: + + async def last_price_for_pair(trading_pair): + symbol = trading_pair_to_symbol(trading_pair) + async with aiohttp.ClientSession() as client: + async with client.get(BeaxyConstants.PublicApi.RATE_URL.format(symbol=symbol)) as response: + response: aiohttp.ClientResponse + if response.status != 200: + raise IOError(f'Error fetching Beaxy market trade for {trading_pair}. ' + f'HTTP status is {response.status}.') + data: Dict[str, Any] = await response.json() + return trading_pair, float(data['price']) + + fetches = [last_price_for_pair(p) for p in trading_pairs] + + prices = await asyncio.gather(*fetches) + + return {pair: price for pair, price in prices} + + @staticmethod + async def get_snapshot(client: aiohttp.ClientSession, trading_pair: str, depth: int = 20) -> Dict[str, Any]: + assert depth in [5, 10, 20] + + # at Beaxy all pairs listed without splitter + symbol = trading_pair_to_symbol(trading_pair) + + async with client.get(BeaxyConstants.PublicApi.ORDER_BOOK_URL.format(symbol=symbol, depth=depth)) as response: + response: aiohttp.ClientResponse + if response.status != 200: + raise IOError(f'Error fetching Beaxy market snapshot for {trading_pair}. ' + f'HTTP status is {response.status}.') + + if not await response.text(): # if test is empty it marks that there is no rows + return { + 'timestamp': 1, + 'entries': [], + 'sequenceNumber': 1, + } + + data: Dict[str, Any] = await response.json() + return data + + async def get_new_order_book(self, trading_pair: str) -> OrderBook: + async with aiohttp.ClientSession() as client: + snapshot: Dict[str, Any] = await self.get_snapshot(client, trading_pair, 20) + snapshot_timestamp = snapshot['timestamp'] + snapshot_msg: OrderBookMessage = BeaxyOrderBook.snapshot_message_from_exchange( + snapshot, + snapshot_timestamp, + metadata={'trading_pair': trading_pair} + ) + order_book: OrderBook = self.order_book_create_function() + active_order_tracker: BeaxyActiveOrderTracker = BeaxyActiveOrderTracker() + bids, asks = active_order_tracker.convert_snapshot_message_to_order_book_row(snapshot_msg) + order_book.apply_snapshot(bids, asks, snapshot_msg.update_id) + return order_book + + async def get_tracking_pairs(self) -> Dict[str, OrderBookTrackerEntry]: + async with aiohttp.ClientSession() as client: + trading_pairs: Optional[List[str]] = await self.get_trading_pairs() + assert trading_pairs is not None + retval: Dict[str, OrderBookTrackerEntry] = {} + number_of_pairs: int = len(trading_pairs) + for index, trading_pair in enumerate(trading_pairs): + try: + snapshot: Dict[str, Any] = await self.get_snapshot(client, trading_pair, 20) + snapshot_timestamp = snapshot['timestamp'] + snapshot_msg: OrderBookMessage = BeaxyOrderBook.snapshot_message_from_exchange( + snapshot, + snapshot_timestamp, + metadata={'trading_pair': trading_pair} + ) + order_book: OrderBook = self.order_book_create_function() + active_order_tracker: BeaxyActiveOrderTracker = BeaxyActiveOrderTracker() + bids, asks = active_order_tracker.convert_snapshot_message_to_order_book_row(snapshot_msg) + order_book.apply_snapshot(bids, asks, snapshot_msg.update_id) + retval[trading_pair] = BeaxyOrderBookTrackerEntry( + trading_pair, + snapshot_timestamp, + order_book, + active_order_tracker + ) + + self.logger().info(f'Initialized order book for {trading_pair}. ' + f'{index+1}/{number_of_pairs} completed.') + except Exception: + self.logger().error(f'Error getting snapshot for {trading_pair}. ', exc_info=True) + await asyncio.sleep(5.0) + return retval + + async def _inner_messages( + self, + ws: websockets.WebSocketClientProtocol + ) -> AsyncIterable[str]: + # Terminate the recv() loop as soon as the next message timed out, so the outer loop can reconnect. + try: + while True: + try: + msg: str = await asyncio.wait_for(ws.recv(), timeout=self.MESSAGE_TIMEOUT) + yield msg + except asyncio.TimeoutError: + try: + pong_waiter = await ws.ping() + await asyncio.wait_for(pong_waiter, timeout=self.PING_TIMEOUT) + except asyncio.TimeoutError: + raise + except asyncio.TimeoutError: + self.logger().warning('WebSocket ping timed out. Going to reconnect...') + return + except ConnectionClosed: + return + finally: + await ws.close() + + async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + # As Beaxy orderbook stream go in one ws and it need to be consistent, + # tracking is done only from listen_for_order_book_snapshots + pass + + async def listen_for_order_book_snapshots(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + while True: + try: + # at Beaxy all pairs listed without splitter + trading_pairs = [trading_pair_to_symbol(p) for p in self._trading_pairs] + + ws_path: str = '/'.join([f'{trading_pair}@depth20' for trading_pair in trading_pairs]) + stream_url: str = f'{BeaxyConstants.PublicApi.WS_BASE_URL}/book/{ws_path}' + + async with websockets.connect(stream_url) as ws: + ws: websockets.WebSocketClientProtocol = ws + async for raw_msg in self._inner_messages(ws): + msg = json.loads(raw_msg) # ujson may round floats uncorrectly + msg_type = msg['type'] + if msg_type == ORDERBOOK_MESSAGE_DIFF: + order_book_message: OrderBookMessage = BeaxyOrderBook.diff_message_from_exchange( + msg, msg['timestamp']) + output.put_nowait(order_book_message) + if msg_type == ORDERBOOK_MESSAGE_SNAPSHOT: + order_book_message: OrderBookMessage = BeaxyOrderBook.snapshot_message_from_exchange( + msg, msg['timestamp']) + output.put_nowait(order_book_message) + 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_trades(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + while True: + try: + # at Beaxy all pairs listed without splitter + trading_pairs = [trading_pair_to_symbol(p) for p in self._trading_pairs] + + ws_path: str = '/'.join([trading_pair for trading_pair in trading_pairs]) + stream_url: str = f'{BeaxyConstants.PublicApi.WS_BASE_URL}/trades/{ws_path}' + + async with websockets.connect(stream_url) as ws: + ws: websockets.WebSocketClientProtocol = ws + async for raw_msg in self._inner_messages(ws): + msg = ujson.loads(raw_msg) + trade_msg: OrderBookMessage = BeaxyOrderBook.trade_message_from_exchange(msg) + output.put_nowait(trade_msg) + 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) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_api_user_stream_data_source.py b/hummingbot/connector/exchange/beaxy/beaxy_api_user_stream_data_source.py new file mode 100644 index 0000000000..9a4b1cb108 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_api_user_stream_data_source.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +import logging +import asyncio +import time +import json +import websockets + +from typing import AsyncIterable, Optional, List +from websockets.exceptions import ConnectionClosed + +from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource +from hummingbot.core.utils.async_utils import safe_gather + +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth +from hummingbot.connector.exchange.beaxy.beaxy_constants import BeaxyConstants + + +class BeaxyAPIUserStreamDataSource(UserStreamTrackerDataSource): + MESSAGE_TIMEOUT = 30.0 + PING_TIMEOUT = 10.0 + + _bxyausds_logger: Optional[logging.Logger] = None + + @classmethod + def logger(cls) -> logging.Logger: + if cls._bxyausds_logger is None: + cls._bxyausds_logger = logging.getLogger(__name__) + return cls._bxyausds_logger + + def __init__(self, beaxy_auth: BeaxyAuth, trading_pairs: Optional[List[str]] = []): + self._beaxy_auth: BeaxyAuth = beaxy_auth + self._trading_pairs = trading_pairs + self._current_listen_key = None + self._listen_for_user_stream_task = None + self._last_recv_time: float = 0 + super().__init__() + + @property + def last_recv_time(self) -> float: + return self._last_recv_time + + async def __listen_ws(self, url: str): + while True: + try: + token = await self._beaxy_auth.get_token() + async with websockets.connect(url.format(access_token=token)) as ws: + async for raw_msg in self._inner_messages(ws): + msg = json.loads(raw_msg) # ujson may round floats uncorrectly + if msg.get('type') == 'keep_alive': + continue + yield msg + except asyncio.CancelledError: + raise + except Exception: + self.logger().error('Unexpected error with Beaxy connection. ' + 'Retrying after 30 seconds...', exc_info=True) + await asyncio.sleep(30.0) + + async def _listen_for_balance(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + async for msg in self.__listen_ws(BeaxyConstants.TradingApi.WS_BALANCE_ENDPOINT): + output.put_nowait([BeaxyConstants.UserStream.BALANCE_MESSAGE, msg]) + + async def _listen_for_orders(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + async for msg in self.__listen_ws(BeaxyConstants.TradingApi.WS_ORDERS_ENDPOINT): + output.put_nowait([BeaxyConstants.UserStream.ORDER_MESSAGE, msg]) + + async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): + """ + *required + Subscribe to user stream via web socket, and keep the connection open for incoming messages + :param ev_loop: ev_loop to execute this function in + :param output: an async queue where the incoming messages are stored + """ + await safe_gather( + self._listen_for_balance(ev_loop, output), + self._listen_for_orders(ev_loop, output), + ) + + async def _inner_messages(self, + ws: websockets.WebSocketClientProtocol) -> AsyncIterable[str]: + """ + Generator function that returns messages from the web socket stream + :param ws: current web socket connection + :returns: message in AsyncIterable format + """ + # Terminate the recv() loop as soon as the next message timed out, so the outer loop can reconnect. + try: + while True: + try: + msg: str = await asyncio.wait_for(ws.recv(), timeout=self.MESSAGE_TIMEOUT) + self._last_recv_time = time.time() + yield msg + except asyncio.TimeoutError: + try: + pong_waiter = await ws.ping() + self._last_recv_time = time.time() + await asyncio.wait_for(pong_waiter, timeout=self.PING_TIMEOUT) + except asyncio.TimeoutError: + raise + except asyncio.TimeoutError: + self.logger().warning('WebSocket ping timed out. Going to reconnect...') + return + except ConnectionClosed: + return + finally: + await ws.close() diff --git a/hummingbot/connector/exchange/beaxy/beaxy_auth.py b/hummingbot/connector/exchange/beaxy/beaxy_auth.py new file mode 100644 index 0000000000..c9c76d733d --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_auth.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +import logging +import asyncio +from typing import Dict, Any, Optional +from time import monotonic +from datetime import datetime + +import aiohttp + +from hummingbot.core.utils.async_utils import safe_gather +from hummingbot.logger import HummingbotLogger + +from hummingbot.connector.exchange.beaxy.beaxy_constants import BeaxyConstants + +s_logger = None + +SAFE_TIME_PERIOD_SECONDS = 10 +TOKEN_REFRESH_PERIOD_SECONDS = 10 * 60 +MIN_TOKEN_LIFE_TIME_SECONDS = 30 +TOKEN_OBTAIN_TIMEOUT = 30 + + +class BeaxyAuth: + + def __init__(self, api_key: str, api_secret: str): + self.api_key = api_key + self.api_secret = api_secret + self._session_data_cache: Dict[str, Any] = {} + + self.token: Optional[str] = None + self.token_obtain = asyncio.Event() + self.token_obtain_started = False + self.token_valid_to: float = 0 + self.token_next_refresh: float = 0 + self.token_obtain_start_time = 0 + self.token_raw_expires = 0 + + @classmethod + def logger(cls) -> HummingbotLogger: + global s_logger + if s_logger is None: + s_logger = logging.getLogger(__name__) + return s_logger + + def is_token_valid(self): + return self.token_valid_to > monotonic() + + def token_timings_str(self): + return f'auth req start time {self.token_obtain_start_time}, token validness sec {self.token_raw_expires}' + + def invalidate_token(self): + self.token_valid_to = 0 + + async def get_token(self): + + for _ in range(3): + if self.is_token_valid(): + return self.token + + # token is invalid, waiting for a renew + if not self.token_obtain_started: + # if process of refreshing is not started, start it + await self._update_token() + + if not self.is_token_valid(): + continue + return self.token + + # waiting for fresh token + await asyncio.wait_for(self.token_obtain.wait(), timeout=TOKEN_OBTAIN_TIMEOUT) + + if not self.is_token_valid(): + continue + return self.token + + raise ValueError('Invalid auth token timestamp') + + async def _update_token(self): + + self.token_obtain.clear() + self.token_obtain_started = True + + try: + + start_time = monotonic() + start_timestamp = datetime.now() + + async with aiohttp.ClientSession() as client: + async with client.post( + f'{BeaxyConstants.TradingApi.BASE_URL}{BeaxyConstants.TradingApi.TOKEN_ENDPOINT}', + json={'api_key_id': self.api_key, 'api_secret': self.api_secret} + ) as response: + response: aiohttp.ClientResponse = response + if response.status != 200: + raise IOError(f'Error while connecting to login token endpoint. HTTP status is {response.status}.') + data: Dict[str, str] = await response.json() + + if data['type'] != 'Bearer': + raise IOError(f'Error while connecting to login token endpoint. Token type is {data["type"]}.') + + if int(data['expires_in']) < MIN_TOKEN_LIFE_TIME_SECONDS: + raise IOError(f'Error while connecting to login token endpoint. Token lifetime to small {data["expires_in"]}.') + + self.token = data['access_token'] + self.token_raw_expires = data['expires_in'] + + # include safe interval, e.g. time that approx network request can take + self.token_obtain_start_time = start_timestamp + self.token_valid_to = start_time + int(data['expires_in']) - SAFE_TIME_PERIOD_SECONDS + self.token_next_refresh = start_time + TOKEN_REFRESH_PERIOD_SECONDS + + if not self.is_token_valid(): + raise ValueError('Invalid auth token timestamp') + + self.token_obtain.set() + + finally: + self.token_obtain_started = False + + async def _auth_token_polling_loop(self): + """ + Separate background process that periodically regenerates auth token + """ + while True: + try: + await safe_gather(self._update_token()) + await asyncio.sleep(TOKEN_REFRESH_PERIOD_SECONDS) + except asyncio.CancelledError: + raise + except Exception: + self.logger().network( + 'Unexpected error while fetching auth token.', + exc_info=True, + app_warning_msg='Could not fetch trading rule updates on Beaxy. ' + 'Check network connection.' + ) + await asyncio.sleep(0.5) + + async def generate_auth_dict(self, http_method: str, path: str, body: str = '') -> Dict[str, Any]: + auth_token = await self.get_token() + return {'Authorization': f'Bearer {auth_token}'} diff --git a/hummingbot/connector/exchange/beaxy/beaxy_constants.py b/hummingbot/connector/exchange/beaxy/beaxy_constants.py new file mode 100644 index 0000000000..5077903fc9 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_constants.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + + +class BeaxyConstants: + class UserStream: + ORDER_MESSAGE = 'ORDER' + BALANCE_MESSAGE = 'BALANCE' + + class TradingApi: + BASE_URL_V1 = 'https://tradingapi.beaxy.com' + BASE_URL = 'https://tradewith.beaxy.com' + HEALTH_ENDPOINT = '/api/v2/health' + TOKEN_ENDPOINT = '/api/v2/auth' + WALLETS_ENDPOINT = '/api/v2/wallets' + OPEN_ORDERS_ENDPOINT = '/api/v2/orders/open' + CLOSED_ORDERS_ENDPOINT = '/api/v2/orders/closed?from_date={from_date}' + DELETE_ORDER_ENDPOINT = '/api/v2/orders/open/{id}' + CREATE_ORDER_ENDPOINT = '/api/v2/orders' + TRADE_SETTINGS_ENDPOINT = '/api/v2/tradingsettings' + + WS_BASE_URL = 'wss://tradewith.beaxy.com' + WS_ORDERS_ENDPOINT = WS_BASE_URL + '/ws/v2/orders?access_token={access_token}' + WS_BALANCE_ENDPOINT = WS_BASE_URL + '/ws/v2/wallets?access_token={access_token}' + + class PublicApi: + BASE_URL = 'https://services.beaxy.com' + SYMBOLS_URL = BASE_URL + '/api/v2/symbols' + RATE_URL = BASE_URL + '/api/v2/symbols/{symbol}/rate' + RATES_URL = BASE_URL + '/api/v2/symbols/rates' + ORDER_BOOK_URL = BASE_URL + '/api/v2/symbols/{symbol}/book?depth={depth}' + + WS_BASE_URL = 'wss://services.beaxy.com/ws/v2' diff --git a/hummingbot/connector/exchange/beaxy/beaxy_exchange.pxd b/hummingbot/connector/exchange/beaxy/beaxy_exchange.pxd new file mode 100644 index 0000000000..4ad2c6ede4 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_exchange.pxd @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from hummingbot.connector.exchange_base cimport ExchangeBase +from hummingbot.core.data_type.transaction_tracker cimport TransactionTracker + + +cdef class BeaxyExchange(ExchangeBase): + cdef: + object _user_stream_tracker + object _beaxy_auth + object _ev_loop + object _poll_notifier + double _last_timestamp + double _last_order_update_timestamp + double _last_fee_percentage_update_timestamp + object _maker_fee_percentage + object _taker_fee_percentage + double _poll_interval + dict _in_flight_orders + dict _order_not_found_records + TransactionTracker _tx_tracker + dict _trading_rules + object _coro_queue + public object _auth_polling_task + public object _status_polling_task + public object _coro_scheduler_task + public object _user_stream_tracker_task + public object _user_stream_event_listener_task + public object _trading_rules_polling_task + public object _shared_client + + cdef c_start_tracking_order(self, + str order_id, + str trading_pair, + object trade_type, + object order_type, + object price, + object amount) + cdef c_did_timeout_tx(self, str tracking_id) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_exchange.pyx b/hummingbot/connector/exchange/beaxy/beaxy_exchange.pyx new file mode 100644 index 0000000000..53f4d09608 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_exchange.pyx @@ -0,0 +1,1227 @@ +# -*- coding: utf-8 -*- + +import asyncio +import logging +import json + +from typing import Any, Dict, List, AsyncIterable, Optional, Tuple +from datetime import datetime, timedelta +from async_timeout import timeout +from decimal import Decimal +from libc.stdint cimport int64_t + +import aiohttp +import pandas as pd + +from aiohttp.client_exceptions import ContentTypeError + +from hummingbot.logger import HummingbotLogger +from hummingbot.core.network_iterator import NetworkStatus +from hummingbot.core.data_type.order_book cimport OrderBook +from hummingbot.core.data_type.cancellation_result import CancellationResult +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.clock cimport Clock +from hummingbot.core.utils.tracking_nonce import get_tracking_nonce +from hummingbot.core.utils.estimate_fee import estimate_fee +from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather +from hummingbot.connector.exchange_base cimport ExchangeBase +from hummingbot.connector.trading_rule cimport TradingRule +from hummingbot.core.event.events import MarketEvent, BuyOrderCompletedEvent, SellOrderCompletedEvent, \ + OrderFilledEvent, OrderCancelledEvent, BuyOrderCreatedEvent, OrderExpiredEvent, SellOrderCreatedEvent, \ + MarketTransactionFailureEvent, MarketOrderFailureEvent, OrderType, TradeType, TradeFee + +from hummingbot.connector.exchange.beaxy.beaxy_api_order_book_data_source import BeaxyAPIOrderBookDataSource +from hummingbot.connector.exchange.beaxy.beaxy_constants import BeaxyConstants +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth +from hummingbot.connector.exchange.beaxy.beaxy_order_book_tracker import BeaxyOrderBookTracker +from hummingbot.connector.exchange.beaxy.beaxy_in_flight_order import BeaxyInFlightOrder +from hummingbot.connector.exchange.beaxy.beaxy_user_stream_tracker import BeaxyUserStreamTracker +from hummingbot.connector.exchange.beaxy.beaxy_misc import split_trading_pair, trading_pair_to_symbol, BeaxyIOError + +s_logger = None +s_decimal_0 = Decimal('0.0') +s_decimal_NaN = Decimal('NaN') + +cdef class BeaxyExchangeTransactionTracker(TransactionTracker): + cdef: + BeaxyExchange _owner + + def __init__(self, owner: BeaxyExchange): + 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 BeaxyExchange(ExchangeBase): + MARKET_BUY_ORDER_COMPLETED_EVENT_TAG = MarketEvent.BuyOrderCompleted.value + MARKET_SELL_ORDER_COMPLETED_EVENT_TAG = MarketEvent.SellOrderCompleted.value + MARKET_ORDER_CANCELLED_EVENT_TAG = MarketEvent.OrderCancelled.value + MARKET_ORDER_FAILURE_EVENT_TAG = MarketEvent.OrderFailure.value + MARKET_ORDER_EXPIRED_EVENT_TAG = MarketEvent.OrderExpired.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 + + API_CALL_TIMEOUT = 60.0 + UPDATE_ORDERS_INTERVAL = 15.0 + UPDATE_FEE_PERCENTAGE_INTERVAL = 60.0 + 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, + beaxy_api_key: str, + beaxy_secret_key: str, + poll_interval: float = 5.0, # interval which the class periodically pulls status from the rest API + trading_pairs: Optional[List[str]] = None, + trading_required: bool = True + ): + super().__init__() + self._trading_required = trading_required + self._beaxy_auth = BeaxyAuth(beaxy_api_key, beaxy_secret_key) + self._order_book_tracker = BeaxyOrderBookTracker(trading_pairs=trading_pairs) + self._order_not_found_records = {} + self._user_stream_tracker = BeaxyUserStreamTracker(beaxy_auth=self._beaxy_auth) + self._ev_loop = asyncio.get_event_loop() + self._poll_notifier = asyncio.Event() + self._last_timestamp = 0 + self._last_order_update_timestamp = 0 + self._last_fee_percentage_update_timestamp = 0 + self._poll_interval = poll_interval + self._in_flight_orders: Dict[str, BeaxyInFlightOrder] = {} + self._tx_tracker = BeaxyExchangeTransactionTracker(self) + self._trading_rules = {} + self._auth_polling_task = None + 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._shared_client = None + self._maker_fee_percentage = {} + self._taker_fee_percentage = {} + + @staticmethod + def split_trading_pair(trading_pair: str) -> Optional[Tuple[str, str]]: + return split_trading_pair(trading_pair) + + @staticmethod + def convert_from_exchange_trading_pair(exchange_trading_pair: str) -> Optional[str]: + if BeaxyExchange.split_trading_pair(exchange_trading_pair) is None: + return None + base_asset, quote_asset = BeaxyExchange.split_trading_pair(exchange_trading_pair) + return f'{base_asset}-{quote_asset}' + + @staticmethod + def convert_to_exchange_trading_pair(hb_trading_pair: str) -> str: + return hb_trading_pair + + @property + def name(self) -> str: + """ + *required + :return: A lowercase name / id for the market. Must stay consistent with market name in global settings. + """ + return 'beaxy' + + @property + def order_books(self) -> Dict[str, OrderBook]: + """ + *required + Get mapping of all the order books that are being tracked. + :return: Dict[trading_pair : OrderBook] + """ + return self._order_book_tracker.order_books + + @property + def beaxy_auth(self) -> BeaxyAuth: + """ + :return: BeaxyAuth class + """ + return self._beaxy_auth + + @property + def trading_rules(self) -> Dict[str, Any]: + return self._trading_rules + + @property + def status_dict(self) -> Dict[str, bool]: + """ + *required + :return: a dictionary of relevant status checks. + This is used by `ready` method below to determine if a market is ready for trading. + """ + 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 if self._trading_required else True + } + + @property + def ready(self) -> bool: + """ + *required + :return: a boolean value that indicates if the market is ready for trading + """ + return all(self.status_dict.values()) + + @property + def limit_orders(self) -> List[LimitOrder]: + """ + *required + :return: list of active limit orders + """ + return [ + in_flight_order.to_limit_order() + for in_flight_order in self._in_flight_orders.values() + ] + + @property + def in_flight_orders(self) -> Dict[str, BeaxyInFlightOrder]: + return self._in_flight_orders + + @property + def tracking_states(self) -> Dict[str, any]: + """ + *required + :return: Dict[client_order_id: InFlightOrder] + This is used by the MarketsRecorder class to orchestrate market classes at a higher level. + """ + return { + key: value.to_json() + for key, value in self._in_flight_orders.items() + } + + def restore_tracking_states(self, saved_states: Dict[str, any]): + """ + *required + Updates inflight order statuses from API results + This is used by the MarketsRecorder class to orchestrate market classes at a higher level. + """ + self._in_flight_orders.update({ + key: BeaxyInFlightOrder.from_json(value) + for key, value in saved_states.items() + }) + + async def get_active_exchange_markets(self) -> pd.DataFrame: + """ + *required + Used by the discovery strategy to read order books of all actively trading markets, + and find opportunities to profit + """ + return await BeaxyAPIOrderBookDataSource.get_active_exchange_markets() + + cdef c_start(self, Clock clock, double timestamp): + """ + *required + c_start function used by top level Clock to orchestrate components of the bot + """ + self._tx_tracker.c_start(clock, timestamp) + ExchangeBase.c_start(self, clock, timestamp) + + async def start_network(self): + """ + *required + Async function used by NetworkBase class to handle when a single market goes online + """ + self.logger().debug(f'Starting beaxy network. Trading required is {self._trading_required}') + self._stop_network() + self._order_book_tracker.start() + self.logger().debug('OrderBookTracker started, starting polling tasks.') + if self._trading_required: + self._auth_polling_task = safe_ensure_future(self._beaxy_auth._auth_token_polling_loop()) + self._status_polling_task = safe_ensure_future(self._status_polling_loop()) + self._trading_rules_polling_task = safe_ensure_future(self._trading_rules_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()) + + async def check_network(self) -> NetworkStatus: + try: + res = await self._api_request(http_method='GET', path_url=BeaxyConstants.TradingApi.HEALTH_ENDPOINT, is_auth_required=False) + if res['trading_server'] != 200 and res['historical_data_server'] != 200: + return NetworkStatus.STOPPED + except asyncio.CancelledError: + raise + except Exception: + self.logger().network('Error fetching Beaxy network status.', exc_info=True) + return NetworkStatus.NOT_CONNECTED + return NetworkStatus.CONNECTED + + cdef c_tick(self, double timestamp): + """ + *required + Used by top level Clock to orchestrate components of the bot. + This function is called frequently with every clock tick + """ + cdef: + int64_t last_tick = (self._last_timestamp / self._poll_interval) + int64_t current_tick = (timestamp / self._poll_interval) + + ExchangeBase.c_tick(self, timestamp) + if current_tick > last_tick: + if not self._poll_notifier.is_set(): + self._poll_notifier.set() + self._last_timestamp = timestamp + + def _stop_network(self): + """ + Synchronous function that handles when a single market goes offline + """ + 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 list_orders(self) -> List[Any]: + """ + Gets a list of the user's active orders via rest API + :returns: json response + """ + + if self._in_flight_orders: + from_date = min(order.created_at for order in self._in_flight_orders.values()) + else: + from_date = datetime.utcnow() - timedelta(minutes=5) + + result = await safe_gather( + self._api_request('get', path_url=BeaxyConstants.TradingApi.OPEN_ORDERS_ENDPOINT), + self._api_request('get', path_url=BeaxyConstants.TradingApi.CLOSED_ORDERS_ENDPOINT.format( + from_date=from_date.strftime('%Y-%m-%dT%H:%M:%SZ') + )), + ) + return result + + async def _update_order_status(self): + """ + Pulls the rest API for for latest order statuses and update local order statuses. + """ + cdef: + double current_timestamp = self._current_timestamp + + if current_timestamp - self._last_order_update_timestamp <= self.UPDATE_ORDERS_INTERVAL: + return + + tracked_orders = list(self._in_flight_orders.values()) + open_orders, closed_orders = await self.list_orders() + open_order_dict = {entry['order_id']: entry for entry in open_orders} + close_order_dict = {entry['order_id']: entry for entry in closed_orders} + + for tracked_order in tracked_orders: + client_order_id = tracked_order.client_order_id + + # Do nothing, if the order has already been cancelled or has failed + if client_order_id not in self._in_flight_orders: + continue + + # get last exchange_order_id with no blocking + exchange_order_id = self._in_flight_orders[client_order_id].exchange_order_id + + if exchange_order_id is None: + continue + + open_order = open_order_dict.get(exchange_order_id) + closed_order = close_order_dict.get(exchange_order_id) + + order_update = closed_order or open_order + + if not open_order and not closed_order: + + 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 for a few times before actually treating + continue + + self.logger().info( + f'The tracked order {client_order_id} does not exist on Beaxy for last day. ' + f'(retried {self._order_not_found_records[client_order_id]}) Removing from tracking.' + ) + tracked_order.last_state = 'CLOSED' + self.c_trigger_event( + self.MARKET_ORDER_CANCELLED_EVENT_TAG, + OrderCancelledEvent(self._current_timestamp, client_order_id) + ) + self.c_stop_tracking_order(client_order_id) + del self._order_not_found_records[client_order_id] + continue + + # Update the tracked order + tracked_order.last_state = order_update['order_status'] + + if order_update['filled_size']: + execute_price = Decimal(order_update['limit_price'] if order_update['limit_price'] else order_update['average_price']) + execute_amount_diff = Decimal(order_update['filled_size']) - tracked_order.executed_amount_base + + # Emit event if executed amount is greater than 0. + if execute_amount_diff > s_decimal_0: + + tracked_order.executed_amount_base = execute_amount_diff + tracked_order.executed_amount_quote += execute_amount_diff * execute_price + + order_type_description = tracked_order.order_type_description + order_filled_event = OrderFilledEvent( + self._current_timestamp, + tracked_order.client_order_id, + tracked_order.trading_pair, + tracked_order.trade_type, + tracked_order.order_type, + execute_price, + execute_amount_diff, + self.c_get_fee( + tracked_order.base_asset, + tracked_order.quote_asset, + tracked_order.order_type, + tracked_order.trade_type, + execute_price, + execute_amount_diff, + ), + exchange_trade_id=exchange_order_id, + ) + self.logger().info(f'Filled {execute_amount_diff} out of {tracked_order.amount} of the ' + f'{order_type_description} order {client_order_id}.') + self.c_trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event) + + if tracked_order.is_done: + if not tracked_order.is_failure and not tracked_order.is_cancelled: + + new_confirmed_amount = Decimal(order_update['size']) + execute_amount_diff = new_confirmed_amount - tracked_order.executed_amount_base + execute_price = Decimal(order_update['limit_price'] if order_update['limit_price'] else order_update['average_price']) + + # Emit event if executed amount is greater than 0. + if execute_amount_diff > s_decimal_0: + + tracked_order.executed_amount_base = execute_amount_diff + tracked_order.executed_amount_quote += execute_amount_diff * execute_price + + order_type_description = tracked_order.order_type_description + order_filled_event = OrderFilledEvent( + self._current_timestamp, + tracked_order.client_order_id, + tracked_order.trading_pair, + tracked_order.trade_type, + tracked_order.order_type, + execute_price, + execute_amount_diff, + self.c_get_fee( + tracked_order.base_asset, + tracked_order.quote_asset, + tracked_order.order_type, + tracked_order.trade_type, + execute_price, + execute_amount_diff, + ), + exchange_trade_id=exchange_order_id, + ) + self.logger().info(f'Filled {execute_amount_diff} out of {tracked_order.amount} of the ' + f'{order_type_description} order {client_order_id}.') + self.c_trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event) + + if tracked_order.trade_type == 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, + tracked_order.client_order_id, + tracked_order.base_asset, + tracked_order.quote_asset, + (tracked_order.fee_asset + or tracked_order.base_asset), + tracked_order.executed_amount_base, + tracked_order.executed_amount_quote, + tracked_order.fee_paid, + tracked_order.order_type)) + else: + self.logger().info(f'The market sell order {tracked_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, + tracked_order.client_order_id, + tracked_order.base_asset, + tracked_order.quote_asset, + (tracked_order.fee_asset + or tracked_order.quote_asset), + tracked_order.executed_amount_base, + tracked_order.executed_amount_quote, + tracked_order.fee_paid, + tracked_order.order_type)) + else: + self.logger().info(f'The market order {tracked_order.client_order_id} has failed/been cancelled ' + f'according to order status API.') + tracked_order.last_state = 'cancelled' + self.c_trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG, + OrderCancelledEvent( + self._current_timestamp, + tracked_order.client_order_id + )) + self.c_stop_tracking_order(tracked_order.client_order_id) + self._last_order_update_timestamp = current_timestamp + + async def place_order(self, order_id: str, trading_pair: str, amount: Decimal, is_buy: bool, order_type: OrderType, + price: Decimal): + """ + Async wrapper for placing orders through the rest API. + :returns: json response from the API + """ + path_url = BeaxyConstants.TradingApi.CREATE_ORDER_ENDPOINT + trading_pair = trading_pair_to_symbol(trading_pair) # at Beaxy all pairs listed without splitter + is_limit_type = order_type.is_limit_type() + + data = { + 'comment': order_id, + 'symbol': trading_pair, + 'order_type': 'limit' if is_limit_type else 'market', + 'side': 'buy' if is_buy else 'sell', + 'size': f'{amount:f}', + 'destination': 'MAXI', + } + if is_limit_type: + data['price'] = f'{price:f}' + order_result = await self._api_request('POST', path_url=path_url, data=data) + self.logger().debug(f'Set order result {order_result}') + return order_result + + cdef object c_get_fee( + self, + str base_currency, + str quote_currency, + object order_type, + object order_side, + object amount, + object price + ): + """ + *required + function to calculate fees for a particular order + :returns: TradeFee class that includes fee percentage and flat fees + """ + + cdef: + object maker_fee = self._maker_fee_percentage + object taker_fee = self._taker_fee_percentage + + is_maker = order_type is OrderType.LIMIT_MAKER + pair = f'{base_currency}-{quote_currency}' + fees = maker_fee if is_maker else taker_fee + + if pair not in fees: + self.logger().info(f'Fee for {pair} is not in fee cache') + return estimate_fee('beaxy', is_maker) + + return TradeFee(percent=fees[pair] / Decimal(100)) + + async def execute_buy( + self, + order_id: str, + trading_pair: str, + amount: Decimal, + order_type: OrderType, + price: Optional[Decimal] = s_decimal_0 + ): + """ + Function that takes strategy inputs, auto corrects itself with trading rule, + and submit an API request to place a buy order + """ + cdef: + TradingRule trading_rule = self._trading_rules[trading_pair] + + decimal_amount = self.quantize_order_amount(trading_pair, amount) + decimal_price = self.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: + self.c_start_tracking_order(order_id, trading_pair, order_type, TradeType.BUY, decimal_price, decimal_amount) + order_result = await self.place_order(order_id, trading_pair, decimal_amount, True, order_type, decimal_price) + exchange_order_id = order_result['order_id'] + 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 {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)) + except asyncio.CancelledError: + raise + except Exception: + tracked_order = self._in_flight_orders.get(order_id) + tracked_order.last_state = 'FAILURE' + self.c_stop_tracking_order(order_id) + order_type_str = order_type.name.lower() + self.logger().network( + f'Error submitting buy {order_type_str} order to Beaxy for ' + f'{decimal_amount} {trading_pair} ' + f'{decimal_price}.', + exc_info=True, + app_warning_msg='Failed to submit buy order to Beaxy. 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.MARKET, object price=s_decimal_0, + dict kwargs={}): + """ + *required + Synchronous wrapper that generates a client-side order ID and schedules the buy order. + """ + cdef: + int64_t tracking_nonce = get_tracking_nonce() + str order_id = str(f'HBOT-buy-{trading_pair}-{tracking_nonce}') + + safe_ensure_future(self.execute_buy(order_id, trading_pair, amount, order_type, price)) + return order_id + + async def execute_sell( + self, + order_id: str, + trading_pair: str, + amount: Decimal, + order_type: OrderType, + price: Optional[Decimal] = s_decimal_0 + ): + """ + Function that takes strategy inputs, auto corrects itself with trading rule, + and submit an API request to place a sell order + """ + cdef: + TradingRule trading_rule = self._trading_rules[trading_pair] + + decimal_amount = self.quantize_order_amount(trading_pair, amount) + decimal_price = self.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: + self.c_start_tracking_order(order_id, trading_pair, order_type, TradeType.SELL, decimal_price, decimal_amount) + order_result = await self.place_order(order_id, trading_pair, decimal_amount, False, order_type, decimal_price) + + exchange_order_id = order_result['order_id'] + 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 {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)) + except asyncio.CancelledError: + raise + except Exception: + tracked_order = self._in_flight_orders.get(order_id) + tracked_order.last_state = 'FAILURE' + self.c_stop_tracking_order(order_id) + order_type_str = order_type.name.lower() + self.logger().network( + f'Error submitting sell {order_type_str} order to Beaxy for ' + f'{decimal_amount} {trading_pair} ' + f'{decimal_price if order_type is OrderType.LIMIT else ""}.', + exc_info=True, + app_warning_msg='Failed to submit sell order to Beaxy. 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.MARKET, + object price=s_decimal_0, + dict kwargs={} + ): + """ + *required + Synchronous wrapper that generates a client-side order ID and schedules the sell order. + """ + cdef: + int64_t tracking_nonce = get_tracking_nonce() + str order_id = str(f'HBOT-sell-{trading_pair}-{tracking_nonce}') + safe_ensure_future(self.execute_sell(order_id, trading_pair, amount, order_type, price)) + return order_id + + async def execute_cancel(self, trading_pair: str, order_id: str): + """ + Function that makes API request to cancel an active order + """ + 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.') + path_url = BeaxyConstants.TradingApi.DELETE_ORDER_ENDPOINT.format(id=tracked_order.exchange_order_id) + cancel_result = await self._api_request('delete', path_url=path_url) + return order_id + except asyncio.CancelledError: + raise + except BeaxyIOError as e: + if e.result and 'Active order not found or already cancelled.' in e.result['items']: + # The order was never there to begin with. So cancelling it is a no-op but semantically successful. + self.c_stop_tracking_order(order_id) + self.c_trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG, + OrderCancelledEvent(self._current_timestamp, order_id)) + return order_id + except IOError as ioe: + self.logger().warning(ioe) + except Exception as e: + self.logger().network( + f'Failed to cancel order {order_id}: ', + exc_info=True, + app_warning_msg=f'Failed to cancel the order {order_id} on Beaxy. ' + f'Check API key and network connection.' + ) + return None + + cdef c_cancel(self, str trading_pair, str order_id): + """ + *required + Synchronous wrapper that schedules cancelling an order. + """ + safe_ensure_future(self.execute_cancel(trading_pair, order_id)) + return order_id + + async def cancel_all(self, timeout_seconds: float) -> List[CancellationResult]: + """ + *required + Async function that cancels all active orders. + Used by bot's top level stop and exit commands (cancelling outstanding orders on exit) + :returns: List of CancellationResult which indicates whether each order is successfully cancelled. + """ + incomplete_orders = [o for o in self._in_flight_orders.values() if not o.is_done] + tasks = [self.execute_cancel(o.trading_pair, o.client_order_id) for o in incomplete_orders] + order_id_set = set([o.client_order_id for o in incomplete_orders]) + successful_cancellations = [] + + try: + async with timeout(timeout_seconds): + results = await safe_gather(*tasks, return_exceptions=True) + for client_order_id in results: + if client_order_id: + order_id_set.remove(client_order_id) + successful_cancellations.append(CancellationResult(client_order_id, True)) + except Exception as e: + self.logger().network( + 'Unexpected error cancelling orders.', + exc_info=True, + app_warning_msg='Failed to cancel order on Beaxy exchange. Check API key and network connection.' + ) + + failed_cancellations = [CancellationResult(oid, False) for oid in order_id_set] + return successful_cancellations + failed_cancellations + + async def _update_trade_fees(self): + + cdef: + double current_timestamp = self._current_timestamp + + if current_timestamp - self._last_fee_percentage_update_timestamp <= self.UPDATE_FEE_PERCENTAGE_INTERVAL: + return + + try: + res = await self._api_request('get', BeaxyConstants.TradingApi.TRADE_SETTINGS_ENDPOINT) + for symbol_data in res['symbols']: + symbol = self.convert_from_exchange_trading_pair(symbol_data['name']) + self._maker_fee_percentage[symbol] = Decimal(symbol_data['maker_fee']) + self._taker_fee_percentage[symbol] = Decimal(symbol_data['taker_fee']) + + self._last_fee_percentage_update_timestamp = current_timestamp + except asyncio.CancelledError: + self.logger().warning('Got cancelled error fetching beaxy fees.') + raise + except Exception: + self.logger().network('Error fetching Beaxy trade fees.', exc_info=True, + app_warning_msg='Could not fetch Beaxy trading fees. ' + 'Check network connection.') + raise + + async def _update_balances(self): + cdef: + dict account_info + list balances + str asset_name + set local_asset_names = set(self._account_balances.keys()) + set remote_asset_names = set() + set asset_names_to_remove + + account_balances = await self._api_request('get', path_url=BeaxyConstants.TradingApi.WALLETS_ENDPOINT) + + for balance_entry in account_balances: + asset_name = balance_entry['currency'] + available_balance = Decimal(balance_entry['available_balance']) + total_balance = Decimal(balance_entry['total_balance']) + self._account_available_balances[asset_name] = available_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] + + async def _update_trading_rules(self): + """ + Pulls the API for trading rules (min / max order size, etc) + """ + cdef: + int64_t last_tick = (self._last_timestamp / 60.0) + int64_t current_tick = (self._current_timestamp / 60.0) + + try: + if current_tick > last_tick or len(self._trading_rules) <= 0: + product_info = await self._api_request(http_method='get', url=BeaxyConstants.PublicApi.SYMBOLS_URL, is_auth_required=False) + trading_rules_list = self._format_trading_rules(product_info) + self._trading_rules.clear() + for trading_rule in trading_rules_list: + + # at Beaxy all pairs listed without splitter, so there is need to convert it + trading_pair = '{}-{}'.format(*BeaxyExchange.split_trading_pair(trading_rule.trading_pair)) + + self._trading_rules[trading_pair] = trading_rule + except Exception: + self.logger().warning('Got exception while updating trading rules.', exc_info=True) + + def _format_trading_rules(self, market_dict: Dict[str, Any]) -> List[TradingRule]: + """ + Turns json data from API into TradingRule instances + :returns: List of TradingRule + """ + cdef: + list retval = [] + + for rule in market_dict: + try: + trading_pair = rule.get('symbol') + # Parsing from string doesn't mess up the precision + retval.append(TradingRule(trading_pair, + min_price_increment=Decimal(str(rule.get('tickSize'))), + min_order_size=Decimal(str(rule.get('minimumQuantity'))), + max_order_size=Decimal(str(rule.get('maximumQuantity'))), + min_base_amount_increment=Decimal(str(rule.get('quantityIncrement'))), + min_quote_amount_increment=Decimal(str(rule.get('quantityIncrement'))), + max_price_significant_digits=Decimal(str(rule.get('pricePrecision'))))) + except Exception: + self.logger().error(f'Error parsing the trading_pair rule {rule}. Skipping.', exc_info=True) + return retval + + 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 Beaxy. Check API key and network connection.' + ) + await asyncio.sleep(1.0) + + async def _user_stream_event_listener(self): + async for msg_type, event_message in self._iter_user_event_queue(): + try: + if msg_type == BeaxyConstants.UserStream.BALANCE_MESSAGE: + if event_message['type'] == 'update': + msgs = [event_message['data']] + elif event_message['type'] == 'snapshot': + msgs = event_message['data'] + + for msg in msgs: + asset_name = msg['currency'] + available_balance = Decimal(msg['available_balance']) + total_balance = Decimal(msg['total_balance']) + self._account_available_balances[asset_name] = available_balance + self._account_balances[asset_name] = total_balance + + elif msg_type == BeaxyConstants.UserStream.ORDER_MESSAGE: + order = event_message['data'] + exchange_order_id = order['order_id'] + client_order_id = order['comment'] + order_status = order['order_status'] + + if client_order_id is None: + continue + + tracked_order = self._in_flight_orders.get(client_order_id) + + if tracked_order is None: + self.logger().debug(f'Didn`rt find order with id {client_order_id}') + continue + + if not tracked_order.exchange_order_id: + tracked_order.exchange_order_id = exchange_order_id + + execute_price = s_decimal_0 + execute_amount_diff = s_decimal_0 + + if order_status == 'partially_filled': + order_filled_size = Decimal(order['trade_size']) + execute_price = Decimal(order['trade_price']) + + execute_amount_diff = order_filled_size - tracked_order.executed_amount_base + + if execute_amount_diff > s_decimal_0: + + tracked_order.executed_amount_base = order_filled_size + tracked_order.executed_amount_quote += Decimal(execute_amount_diff * execute_price) + + self.logger().info(f'Filled {execute_amount_diff} out of {tracked_order.amount} of the ' + f'{tracked_order.order_type_description} order {tracked_order.client_order_id}') + + 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, + execute_price, + execute_amount_diff, + self.c_get_fee( + tracked_order.base_asset, + tracked_order.quote_asset, + tracked_order.order_type, + tracked_order.trade_type, + execute_price, + execute_amount_diff, + ), + exchange_trade_id=exchange_order_id + )) + + elif order_status == 'completely_filled': + + new_confirmed_amount = Decimal(order['size']) + execute_amount_diff = new_confirmed_amount - tracked_order.executed_amount_base + execute_price = Decimal(order['limit_price'] if order['limit_price'] else order['average_price']) + + # Emit event if executed amount is greater than 0. + if execute_amount_diff > s_decimal_0: + + tracked_order.executed_amount_base = execute_amount_diff + tracked_order.executed_amount_quote += execute_amount_diff * execute_price + + order_type_description = tracked_order.order_type_description + order_filled_event = OrderFilledEvent( + self._current_timestamp, + tracked_order.client_order_id, + tracked_order.trading_pair, + tracked_order.trade_type, + tracked_order.order_type, + execute_price, + execute_amount_diff, + self.c_get_fee( + tracked_order.base_asset, + tracked_order.quote_asset, + tracked_order.order_type, + tracked_order.trade_type, + execute_price, + execute_amount_diff, + ), + exchange_trade_id=exchange_order_id, + ) + self.logger().info(f'Filled {execute_amount_diff} out of {tracked_order.amount} of the ' + f'{order_type_description} order {client_order_id}.') + self.c_trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event) + + if tracked_order.trade_type == TradeType.BUY: + self.logger().info(f'The market buy order {tracked_order.client_order_id} has completed ' + f'according to Beaxy 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.fee_asset + or tracked_order.base_asset), + tracked_order.executed_amount_base, + tracked_order.executed_amount_quote, + tracked_order.fee_paid, + tracked_order.order_type)) + else: + self.logger().info(f'The market sell order {tracked_order.client_order_id} has completed ' + f'according to Beaxy 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.fee_asset + or tracked_order.quote_asset), + tracked_order.executed_amount_base, + tracked_order.executed_amount_quote, + tracked_order.fee_paid, + tracked_order.order_type)) + + self.c_stop_tracking_order(tracked_order.client_order_id) + + elif order_status == 'canceled': + tracked_order.last_state = 'canceled' + self.c_trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG, + OrderCancelledEvent(self._current_timestamp, tracked_order.client_order_id)) + self.c_stop_tracking_order(tracked_order.client_order_id) + elif order_status in ['rejected', 'replaced', 'suspended']: + tracked_order.last_state = order_status + 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) + elif order_status == 'expired': + tracked_order.last_state = 'expired' + self.c_trigger_event(self.MARKET_ORDER_EXPIRED_EVENT_TAG, + OrderExpiredEvent(self._current_timestamp, tracked_order.client_order_id)) + self.c_stop_tracking_order(tracked_order.client_order_id) + + except Exception: + self.logger().error('Unexpected error in user stream listener loop.', exc_info=True) + await asyncio.sleep(5.0) + + async def _http_client(self) -> aiohttp.ClientSession: + """ + :returns: Shared client session instance + """ + if self._shared_client is None: + self._shared_client = aiohttp.ClientSession() + return self._shared_client + + async def _api_request( + self, + http_method: str, + path_url: str = None, + url: str = None, + is_auth_required: bool = True, + data: Optional[Dict[str, Any]] = None, + custom_headers: [Optional[Dict[str, str]]] = None + ) -> Dict[str, Any]: + """ + A wrapper for submitting API requests to Beaxy + :returns: json data from the endpoints + """ + try: + assert path_url is not None or url is not None + + url = f'{BeaxyConstants.TradingApi.BASE_URL}{path_url}' if url is None else url + + data_str = '' if data is None else json.dumps(data, separators=(',', ':')) + + if is_auth_required: + headers = await self.beaxy_auth.generate_auth_dict(http_method, path_url, data_str) + else: + headers = {'Content-Type': 'application/json'} + + if custom_headers: + headers = {**custom_headers, **headers} + + if http_method.upper() == 'POST': + headers['Content-Type'] = 'application/json; charset=utf-8' + + if path_url == BeaxyConstants.TradingApi.TRADE_SETTINGS_ENDPOINT: + auth_token = await self._beaxy_auth.get_token() + headers['Authorization'] = f'Bearer {auth_token}' + + self.logger().debug(f'Submitting {http_method} request to {url} with headers {headers}') + + client = await self._http_client() + async with client.request(http_method.upper(), url=url, timeout=self.API_CALL_TIMEOUT, data=data_str, headers=headers) as response: + result = None + try: + result = await response.json() + except ContentTypeError: + pass + + if response.status not in [200, 204]: + + if response.status == 401: + self.logger().error(f'Beaxy auth error, token timings: {self._beaxy_auth.token_timings_str()}') + self._beaxy_auth.invalidate_token() + + raise BeaxyIOError( + f'Error during api request with body {data_str}. HTTP status is {response.status}. Response - {await response.text()} - Request {response.request_info}', + response=response, + result=result, + ) + self.logger().debug(f'Got response status {response.status}') + self.logger().debug(f'Got response {result}') + return result + except BeaxyIOError: + raise + except Exception: + self.logger().warning('Exception while making api request.', exc_info=True) + raise + + async def _status_polling_loop(self): + """ + Background process that periodically pulls for changes from the rest API + """ + while True: + try: + + self._poll_notifier = asyncio.Event() + await self._poll_notifier.wait() + + await safe_gather( + # self._update_balances(), # due to balance polling inconsistency, we use only ws balance update + self._update_trade_fees(), + self._update_order_status(), + ) + + 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 on Beaxy.' + 'Check API key and network connection.' + ) + await asyncio.sleep(0.5) + + async def _trading_rules_polling_loop(self): + """ + Separate background process that periodically pulls for trading rule changes + (Since trading rules don't get updated often, it is pulled less often.) + """ + while True: + try: + await safe_gather(self._update_trading_rules()) + await asyncio.sleep(6000) + except asyncio.CancelledError: + raise + except Exception: + self.logger().network( + 'Unexpected error while fetching trading rules.', + exc_info=True, + app_warning_msg='Could not fetch trading rule updates on Beaxy. ' + 'Check network connection.' + ) + await asyncio.sleep(0.5) + + cdef OrderBook c_get_order_book(self, str trading_pair): + """ + :returns: OrderBook for a specific 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_start_tracking_order(self, + str client_order_id, + str trading_pair, + object order_type, + object trade_type, + object price, + object amount): + """ + Add new order to self._in_flight_orders mapping + """ + self._in_flight_orders[client_order_id] = BeaxyInFlightOrder( + client_order_id, + None, + trading_pair, + order_type, + trade_type, + price, + amount, + created_at=datetime.utcnow() + ) + + 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) + ) + + 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): + """ + *required + Check current order amount against trading rule, and correct any rule violations + :return: Valid order amount in Decimal format + """ + cdef: + TradingRule trading_rule = self._trading_rules[trading_pair] + object quantized_amount = ExchangeBase.c_quantize_order_amount(self, trading_pair, amount) + + # Check against min_order_size. If not passing either check, return 0. + if quantized_amount < trading_rule.min_order_size: + return s_decimal_0 + + # Check against max_order_size. If not passing either check, return 0. + if quantized_amount > trading_rule.max_order_size: + return s_decimal_0 + + return quantized_amount + + def supported_order_types(self): + return [OrderType.LIMIT, OrderType.LIMIT_MAKER, OrderType.MARKET] + + cdef c_stop_tracking_order(self, str order_id): + """ + Delete an order from self._in_flight_orders mapping + """ + if order_id in self._in_flight_orders: + del self._in_flight_orders[order_id] + + 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) -> TradeFee: + return self.c_get_fee(base_currency, quote_currency, order_type, order_side, amount, price) + + def get_order_book(self, trading_pair: str) -> OrderBook: + return self.c_get_order_book(trading_pair) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pxd b/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pxd new file mode 100644 index 0000000000..d199b8bada --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pxd @@ -0,0 +1,5 @@ +from hummingbot.connector.in_flight_order_base cimport InFlightOrderBase + +cdef class BeaxyInFlightOrder(InFlightOrderBase): + cdef: + public object created_at diff --git a/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pyx b/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pyx new file mode 100644 index 0000000000..ccbc03cdd0 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_in_flight_order.pyx @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +from decimal import Decimal +from typing import Any, Dict, Optional +from datetime import datetime + +from hummingbot.core.event.events import OrderType, TradeType +from hummingbot.connector.in_flight_order_base import InFlightOrderBase + + +cdef class BeaxyInFlightOrder(InFlightOrderBase): + def __init__( + self, + client_order_id: str, + exchange_order_id: Optional[str], + trading_pair: str, + order_type: OrderType, + trade_type: TradeType, + price: Decimal, + amount: Decimal, + created_at: datetime, + initial_state: str = 'new', + ): + super().__init__( + client_order_id, + exchange_order_id, + trading_pair, + order_type, + trade_type, + price, + amount, + initial_state + ) + self.created_at = created_at + + @property + def is_done(self) -> bool: + return self.last_state in {'closed', 'completely_filled', 'canceled', 'cancelled', 'rejected', 'replaced', 'expired', 'pending_cancel', 'suspended', 'pending_replace'} + + @property + def is_failure(self) -> bool: + # This is the only known canceled state + return self.last_state in {'canceled', 'cancelled', 'pending_cancel', 'rejected', 'expired', 'suspended'} + + @property + def is_cancelled(self) -> bool: + return self.last_state in {'cancelled', 'canceled'} + + @property + def order_type_description(self) -> str: + """ + :return: Order description string . One of ['limit buy' / 'limit sell' / 'market buy' / 'market sell'] + """ + order_type = 'market' if self.order_type is OrderType.MARKET else 'limit' + side = 'buy' if self.trade_type == TradeType.BUY else 'sell' + return f'{order_type} {side}' + + def to_json(self) -> Dict[str, Any]: + return dict( + created_at=self.created_at.isoformat(), + **super(BeaxyInFlightOrder, self).to_json() + ) + + @classmethod + def from_json(cls, data: Dict[str, Any]) -> InFlightOrderBase: + """ + :param data: json data from API + :return: formatted InFlightOrder + """ + cdef: + BeaxyInFlightOrder retval = BeaxyInFlightOrder( + data['client_order_id'], + data['exchange_order_id'], + data['trading_pair'], + getattr(OrderType, data['order_type']), + getattr(TradeType, data['trade_type']), + Decimal(data['price']), + Decimal(data['amount']), + datetime.fromisoformat(data['created_at']), + data['last_state'], + ) + retval.executed_amount_base = Decimal(data['executed_amount_base']) + retval.executed_amount_quote = Decimal(data['executed_amount_quote']) + retval.fee_asset = data['fee_asset'] + retval.fee_paid = Decimal(data['fee_paid']) + retval.last_state = data['last_state'] + return retval diff --git a/hummingbot/connector/exchange/beaxy/beaxy_misc.py b/hummingbot/connector/exchange/beaxy/beaxy_misc.py new file mode 100644 index 0000000000..fa97435385 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_misc.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import re +from typing import Optional, Tuple, List + +TRADING_PAIR_SPLITTER = re.compile(r'^(\w+)?(BTC|ETH|BXY|USDT|USDC|USD)(\w+)?$') + + +def split_trading_pair(trading_pair: str) -> Optional[Tuple[str, str]]: + """ + Converts Beaxy exchange API format to Hummingbot trading pair format + example: BTCUSDC -> BTC-USDC + """ + + try: + if '-' in trading_pair: + return trading_pair.split('-') + + m = TRADING_PAIR_SPLITTER.match(trading_pair) + # determine the main asset + sub_pre, main, sub_post = m.group(1), m.group(2), m.group(3) + # keep ordering like in pair + if sub_pre: + return sub_pre, main + elif sub_post: + return main, sub_post + + # Exceptions are now logged as warnings in trading pair fetcher + except Exception: # nopep8 + return None + + +def symbol_to_trading_pair(trading_pair: str) -> str: + return '{}-{}'.format(*split_trading_pair(trading_pair)) + + +def split_market_pairs(pairs: List[str]): + """ + formats list of Beaxy pairs to Hummingbot trading pair format + """ + for pair in pairs: + formatted = split_trading_pair(pair) + if formatted: + yield formatted + + +def trading_pair_to_symbol(trading_pair: str) -> str: + """ + Converts Hummingbot trading pair format to Beaxy exchange API format + example: BTC-USDC -> BTCUSDC + """ + return trading_pair.replace('-', '') + + +class BeaxyIOError(IOError): + + def __init__(self, msg, response, result, *args, **kwargs): + self.response = response + self.result = result + super(BeaxyIOError, self).__init__(msg, *args, **kwargs) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book.pxd b/hummingbot/connector/exchange/beaxy/beaxy_order_book.pxd new file mode 100644 index 0000000000..2a1984af41 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book.pxd @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from hummingbot.core.data_type.order_book cimport OrderBook + +cdef class BeaxyOrderBook(OrderBook): + pass diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book.pyx b/hummingbot/connector/exchange/beaxy/beaxy_order_book.pyx new file mode 100644 index 0000000000..98a749bdca --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book.pyx @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +import logging +from typing import Dict, Optional, Any, List +from decimal import Decimal + +from hummingbot.logger import HummingbotLogger +from hummingbot.core.event.events import TradeType +from hummingbot.core.data_type.order_book cimport OrderBook +from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType + +from hummingbot.connector.exchange.beaxy.beaxy_order_book_message import BeaxyOrderBookMessage +from hummingbot.connector.exchange.beaxy.beaxy_misc import symbol_to_trading_pair + + +_bxob_logger = None + + +cdef class BeaxyOrderBook(OrderBook): + @classmethod + def logger(cls) -> HummingbotLogger: + global _bxob_logger + if _bxob_logger is None: + _bxob_logger = logging.getLogger(__name__) + return _bxob_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 BeaxyOrderBookMessage( + message_type=OrderBookMessageType.SNAPSHOT, + content=msg, + timestamp=timestamp + ) + + @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 BeaxyOrderBookMessage( + message_type=OrderBookMessageType.DIFF, + content=msg, + timestamp=timestamp + ) + + @classmethod + def trade_message_from_exchange( + cls, + msg: Dict[str, Any], + timestamp: Optional[float] = None, + metadata: Optional[Dict] = None + ) -> OrderBookMessage: + if metadata: + msg.update(metadata) + ts = msg['timestamp'] + return OrderBookMessage(OrderBookMessageType.TRADE, { + 'trading_pair': symbol_to_trading_pair(msg['symbol']), + 'trade_type': float(TradeType.SELL.value) if msg['side'] == 'SELL' else float(TradeType.BUY.value), + 'price': Decimal(str(msg['price'])), + 'update_id': ts, + 'amount': msg['size'] + }, timestamp=ts * 1e-3) + + @classmethod + def from_snapshot(cls, snapshot: OrderBookMessage): + raise NotImplementedError('Beaxy order book needs to retain individual order data.') + + @classmethod + def restore_from_snapshot_and_diffs(self, snapshot: OrderBookMessage, diffs: List[OrderBookMessage]): + raise NotImplementedError('Beaxy order book needs to retain individual order data.') diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book_message.py b/hummingbot/connector/exchange/beaxy/beaxy_order_book_message.py new file mode 100644 index 0000000000..c24887711e --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book_message.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import time +from typing import Dict, List, Optional + +from hummingbot.core.data_type.order_book_row import OrderBookRow +from hummingbot.core.data_type.order_book_message import OrderBookMessage, OrderBookMessageType + +from hummingbot.connector.exchange.beaxy.beaxy_misc import symbol_to_trading_pair + + +class BeaxyOrderBookMessage(OrderBookMessage): + def __new__( + cls, + message_type: OrderBookMessageType, + content: Dict[str, any], + timestamp: Optional[float] = None, + *args, + **kwargs, + ): + if timestamp is None: + if message_type is OrderBookMessageType.SNAPSHOT: + raise ValueError('timestamp must not be None when initializing snapshot messages.') + timestamp = int(time.time()) + return super(BeaxyOrderBookMessage, cls).__new__( + cls, message_type, content, timestamp=timestamp, *args, **kwargs + ) + + @property + def update_id(self) -> int: + return int(str(self.content['sequenceNumber'])) + + @property + def trade_id(self) -> int: + return int(self.timestamp * 1e3) + + @property + def trading_pair(self) -> str: + return symbol_to_trading_pair(str(self.content.get('security'))) + + def _entries(self, side): + return [ + OrderBookRow(entry['price'], entry['quantity'], self.update_id) + if entry['action'] != 'DELETE' else OrderBookRow(entry['price'], 0, self.update_id) + for entry in self.content.get('entries', []) + if entry['side'] == side + ] + + @property + def asks(self) -> List[OrderBookRow]: + return self._entries('ASK') + + @property + def bids(self) -> List[OrderBookRow]: + return self._entries('BID') + + @property + def has_update_id(self) -> bool: + return True + + @property + def has_trade_id(self) -> bool: + return True + + def __eq__(self, other) -> bool: + return self.type == other.type and self.timestamp == other.timestamp + + def __lt__(self, other) -> bool: + if self.timestamp != other.timestamp: + return self.timestamp < other.timestamp + else: + """ + If timestamp is the same, the ordering is snapshot < diff < trade + """ + return self.type.value < other.type.value diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py new file mode 100644 index 0000000000..d730dc97c7 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +import asyncio +import time +import bisect +import logging + +from collections import defaultdict, deque +from typing import Deque, Dict, List, Optional, Set + +from hummingbot.logger import HummingbotLogger +from hummingbot.core.data_type.order_book_tracker import OrderBookTracker +from hummingbot.connector.exchange.beaxy.beaxy_api_order_book_data_source import BeaxyAPIOrderBookDataSource +from hummingbot.connector.exchange.beaxy.beaxy_order_book_message import BeaxyOrderBookMessage +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.core.data_type.order_book_message import OrderBookMessageType +from hummingbot.core.data_type.order_book import OrderBook + +from hummingbot.connector.exchange.beaxy.beaxy_order_book import BeaxyOrderBook +from hummingbot.connector.exchange.beaxy.beaxy_active_order_tracker import BeaxyActiveOrderTracker +from hummingbot.connector.exchange.beaxy.beaxy_order_book_tracker_entry import BeaxyOrderBookTrackerEntry + + +class BeaxyOrderBookTracker(OrderBookTracker): + _bxobt_logger: Optional[HummingbotLogger] = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls._bxobt_logger is None: + cls._bxobt_logger = logging.getLogger(__name__) + return cls._bxobt_logger + + def __init__(self, trading_pairs: List[str]): + super().__init__(BeaxyAPIOrderBookDataSource(trading_pairs), trading_pairs) + + self._ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() + self._order_book_snapshot_stream: asyncio.Queue = asyncio.Queue() + self._order_book_diff_stream: asyncio.Queue = asyncio.Queue() + self._process_msg_deque_task: Optional[asyncio.Task] = None + self._past_diffs_windows: Dict[str, Deque] = {} + self._order_books: Dict[str, BeaxyOrderBook] = {} + self._saved_message_queues: Dict[str, Deque[BeaxyOrderBookMessage]] = defaultdict(lambda: deque(maxlen=1000)) + self._active_order_trackers: Dict[str, BeaxyActiveOrderTracker] = defaultdict(BeaxyActiveOrderTracker) + + @property + def exchange_name(self) -> str: + """ + *required + Name of the current exchange + """ + return 'beaxy' + + 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_queued: int = 0 + messages_accepted: int = 0 + messages_rejected: int = 0 + + while True: + try: + ob_message: BeaxyOrderBookMessage = await self._order_book_diff_stream.get() + trading_pair: str = ob_message.trading_pair + + if trading_pair not in self._tracking_message_queues: + messages_queued += 1 + # Save diff messages received before snapshots are ready + self._saved_message_queues[trading_pair].append(ob_message) + 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('Messages processed: %d, rejected: %d, queued: %d', + messages_accepted, + messages_rejected, + messages_queued) + messages_accepted = 0 + messages_rejected = 0 + messages_queued = 0 + + last_message_timestamp = now + except asyncio.CancelledError: + raise + except Exception: + self.logger().network( + 'Unexpected error routing order book messages.', + exc_info=True, + app_warning_msg='Unexpected error routing order book messages. Retrying after 5 seconds.' + ) + await asyncio.sleep(5.0) + + async def _refresh_tracking_tasks(self): + """ + Starts tracking for any new trading pairs, and stop tracking for any inactive trading pairs. + """ + tracking_trading_pairs: Set[str] = set([key for key in self._tracking_tasks.keys() + if not self._tracking_tasks[key].done()]) + available_pairs: Dict[str, BeaxyOrderBookTrackerEntry] = await self.data_source.get_tracking_pairs() + available_trading_pairs: Set[str] = set(available_pairs.keys()) + new_trading_pairs: Set[str] = available_trading_pairs - tracking_trading_pairs + deleted_trading_pairs: Set[str] = tracking_trading_pairs - available_trading_pairs + + for trading_pair in new_trading_pairs: + order_book_tracker_entry: BeaxyOrderBookTrackerEntry = available_pairs[trading_pair] + self._active_order_trackers[trading_pair] = order_book_tracker_entry.active_order_tracker + self._order_books[trading_pair] = order_book_tracker_entry.order_book + self._tracking_message_queues[trading_pair] = asyncio.Queue() + self._tracking_tasks[trading_pair] = safe_ensure_future(self._track_single_book(trading_pair)) + self.logger().info('Started order book tracking for %s.' % trading_pair) + + for trading_pair in deleted_trading_pairs: + self._tracking_tasks[trading_pair].cancel() + del self._tracking_tasks[trading_pair] + del self._order_books[trading_pair] + del self._active_order_trackers[trading_pair] + del self._tracking_message_queues[trading_pair] + self.logger().info('Stopped order book tracking for %s.' % trading_pair) + + async def _track_single_book(self, trading_pair: str): + """ + Update an order book with changes from the latest batch of received messages + """ + past_diffs_window: Deque[BeaxyOrderBookMessage] = deque() + self._past_diffs_windows[trading_pair] = past_diffs_window + + message_queue: asyncio.Queue = self._tracking_message_queues[trading_pair] + order_book: BeaxyOrderBook = self._order_books[trading_pair] + active_order_tracker: BeaxyActiveOrderTracker = self._active_order_trackers[trading_pair] + + last_message_timestamp: float = time.time() + diff_messages_accepted: int = 0 + + while True: + try: + message: BeaxyOrderBookMessage = None + saved_messages: Deque[BeaxyOrderBookMessage] = self._saved_message_queues[trading_pair] + # Process saved messages first if there are any + if len(saved_messages) > 0: + message = saved_messages.popleft() + else: + message = await message_queue.get() + + if message.type is OrderBookMessageType.DIFF: + bids, asks = active_order_tracker.convert_diff_message_to_order_book_row(message) + order_book.apply_diffs(bids, asks, message.update_id) + past_diffs_window.append(message) + while len(past_diffs_window) > self.PAST_DIFF_WINDOW_SIZE: + past_diffs_window.popleft() + diff_messages_accepted += 1 + + # Output some statistics periodically. + now: float = time.time() + if int(now / 60.0) > int(last_message_timestamp / 60.0): + self.logger().debug('Processed %d order book diffs for %s.', + diff_messages_accepted, trading_pair) + diff_messages_accepted = 0 + last_message_timestamp = now + elif message.type is OrderBookMessageType.SNAPSHOT: + past_diffs: List[BeaxyOrderBookMessage] = list(past_diffs_window) + # only replay diffs later than snapshot, first update active order with snapshot then replay diffs + replay_position = bisect.bisect_right(past_diffs, message) + replay_diffs = past_diffs[replay_position:] + s_bids, s_asks = active_order_tracker.convert_snapshot_message_to_order_book_row(message) + order_book.apply_snapshot(s_bids, s_asks, message.update_id) + for diff_message in replay_diffs: + d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) + order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) + + self.logger().debug('Processed order book snapshot for %s.', trading_pair) + except asyncio.CancelledError: + raise + except Exception: + self.logger().network( + f'Unexpected error processing order book messages for {trading_pair}.', + exc_info=True, + app_warning_msg='Unexpected error processing order book messages. Retrying after 5 seconds.' + ) + await asyncio.sleep(5.0) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker_entry.py b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker_entry.py new file mode 100644 index 0000000000..1d3fb7bc59 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker_entry.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from hummingbot.core.data_type.order_book import OrderBook +from hummingbot.core.data_type.order_book_tracker_entry import OrderBookTrackerEntry + +from hummingbot.connector.exchange.beaxy.beaxy_active_order_tracker import BeaxyActiveOrderTracker + + +class BeaxyOrderBookTrackerEntry(OrderBookTrackerEntry): + def __init__( + self, + trading_pair: str, + timestamp: float, + order_book: OrderBook, + active_order_tracker: BeaxyActiveOrderTracker + ): + self._active_order_tracker = active_order_tracker + super(BeaxyOrderBookTrackerEntry, self).__init__(trading_pair, timestamp, order_book) + + def __repr__(self) -> str: + return ( + f'BeaxyOrderBookTrackerEntry(trading_pair="{self._trading_pair}", timestamp="{self._timestamp}", ' + f'order_book="{self._order_book}")' + ) + + @property + def active_order_tracker(self) -> BeaxyActiveOrderTracker: + return self._active_order_tracker diff --git a/hummingbot/connector/exchange/beaxy/beaxy_user_stream_tracker.py b/hummingbot/connector/exchange/beaxy/beaxy_user_stream_tracker.py new file mode 100644 index 0000000000..c9696fbfc0 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_user_stream_tracker.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +import asyncio +import logging + +from typing import Optional, List + +from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource +from hummingbot.core.data_type.user_stream_tracker import UserStreamTracker +from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather + +from hummingbot.connector.exchange.beaxy.beaxy_api_user_stream_data_source import BeaxyAPIUserStreamDataSource +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth + + +class BeaxyUserStreamTracker(UserStreamTracker): + _bxyust_logger: Optional[logging.Logger] = None + + @classmethod + def logger(cls) -> logging.Logger: + if cls._bxyust_logger is None: + cls._bxyust_logger = logging.getLogger(__name__) + return cls._bxyust_logger + + def __init__( + self, + beaxy_auth: BeaxyAuth, + trading_pairs: Optional[List[str]] = [], + ): + super().__init__() + self._beaxy_auth = beaxy_auth + self._trading_pairs: List[str] = trading_pairs + self._ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() + self._data_source: Optional[UserStreamTrackerDataSource] = None + self._user_stream_tracking_task = None + + @property + def data_source(self) -> UserStreamTrackerDataSource: + if not self._data_source: + self._data_source = BeaxyAPIUserStreamDataSource( + beaxy_auth=self._beaxy_auth, trading_pairs=self._trading_pairs) + return self._data_source + + @property + def exchange_name(self) -> str: + return 'beaxy' + + async def start(self): + self._user_stream_tracking_task = safe_ensure_future( + self.data_source.listen_for_user_stream(self._ev_loop, self._user_stream) + ) + await safe_gather(self._user_stream_tracking_task) diff --git a/hummingbot/connector/exchange/beaxy/beaxy_utils.py b/hummingbot/connector/exchange/beaxy/beaxy_utils.py new file mode 100644 index 0000000000..c7f6aace79 --- /dev/null +++ b/hummingbot/connector/exchange/beaxy/beaxy_utils.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from hummingbot.client.config.config_var import ConfigVar +from hummingbot.client.config.config_methods import using_exchange + + +CENTRALIZED = True + +EXAMPLE_PAIR = 'BTC-USDC' + +DEFAULT_FEES = [0.15, 0.25] + +KEYS = { + 'beaxy_api_key': + ConfigVar(key='beaxy_api_key', + prompt='Enter your Beaxy API key >>> ', + required_if=using_exchange('beaxy'), + is_secure=True, + is_connect_key=True), + 'beaxy_secret_key': + ConfigVar(key='beaxy_secret_key', + prompt='Enter your Beaxy secret key >>> ', + required_if=using_exchange('beaxy'), + is_secure=True, + is_connect_key=True), +} diff --git a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml index dd3f6ca402..c4f1e53d48 100644 --- a/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml +++ b/hummingbot/templates/conf_fee_overrides_TEMPLATE.yml @@ -3,7 +3,7 @@ ######################################## # For more detailed information: https://docs.hummingbot.io -template_version: 5 +template_version: 6 # Exchange trading fees, the values are in percentage value, e.g. 0.1 for 0.1%. # If the value is left blank, the default value (from corresponding market connector) will be used. @@ -11,6 +11,9 @@ template_version: 5 binance_maker_fee: binance_taker_fee: +beaxy_maker_fee: +beaxy_taker_fee: + coinbase_pro_maker_fee: coinbase_pro_taker_fee: diff --git a/hummingbot/templates/conf_global_TEMPLATE.yml b/hummingbot/templates/conf_global_TEMPLATE.yml index e7afe14b40..ebbb32196e 100644 --- a/hummingbot/templates/conf_global_TEMPLATE.yml +++ b/hummingbot/templates/conf_global_TEMPLATE.yml @@ -3,12 +3,15 @@ ################################# # For more detailed information: https://docs.hummingbot.io -template_version: 17 +template_version: 18 # Exchange configs bamboo_relay_use_coordinator: false bamboo_relay_pre_emptive_soft_cancels: false +beaxy_api_key: null +beaxy_secret_key: null + binance_api_key: null binance_api_secret: null diff --git a/setup.py b/setup.py index d1f046b7f3..b3e5dea846 100755 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ def main(): "hummingbot.connector.exchange.liquid", "hummingbot.connector.exchange.dolomite", "hummingbot.connector.exchange.eterbase", + "hummingbot.connector.exchange.beaxy", "hummingbot.connector.exchange.bitmax", "hummingbot.connector.derivative", "hummingbot.connector.derivative.binance_perpetual", diff --git a/test/integration/assets/mock_data/fixture_beaxy.py b/test/integration/assets/mock_data/fixture_beaxy.py new file mode 100644 index 0000000000..8bb12c7dfe --- /dev/null +++ b/test/integration/assets/mock_data/fixture_beaxy.py @@ -0,0 +1,1117 @@ +class FixtureBeaxy: + + BALANCES = [{'id': '20E33E7E-6940-4521-B3CB-DC75FD9DE0EA', 'currency': 'NEO', 'available_balance': '0', 'total_balance': '0'}, {'id': '64BFCB9A-1E19-4A71-AF2E-FE8344F66B56', 'currency': 'AION', 'available_balance': '0', 'total_balance': '0'}, {'id': '1FB1DD29-406B-4227-AE2A-700D8EDCE001', 'currency': 'BXY', 'available_balance': '5572.63464229', 'total_balance': '5572.63464229'}, {'id': 'F801485E-774A-472C-A4D4-CCEEF4AC4C21', 'currency': 'WAVES', 'available_balance': '0', 'total_balance': '0'}, {'id': 'B916B506-177A-4359-86F2-80D1845B22B5', 'currency': 'BEAM', 'available_balance': '0', 'total_balance': '0'}, {'id': 'D3A33644-A624-4421-BAC6-1F92A93A0CB2', 'currency': 'USD', 'available_balance': '0', 'total_balance': '0'}, {'id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'currency': 'BTC', 'available_balance': '0.000000029675', 'total_balance': '0.00070592'}, {'id': '2200FC9C-B559-4C70-9605-926E28193605', 'currency': 'BAT', 'available_balance': '0', 'total_balance': '0'}, {'id': '71AF49AC-54A4-4F5A-A3A1-97CFEFE40430', 'currency': 'XRP', 'available_balance': '0', 'total_balance': '0'}, {'id': '53BDA6C6-5C66-4BD7-BE63-D547699525A4', 'currency': 'DRGN', 'available_balance': '0', 'total_balance': '0'}, {'id': '3D0863E2-7AE9-4478-ABED-1FECAF86B639', 'currency': 'ALEPH', 'available_balance': '0', 'total_balance': '0'}, {'id': '91A87666-6590-45A8-B70F-FA28EFD32716', 'currency': 'POLY', 'available_balance': '0', 'total_balance': '0'}, {'id': '4B4E0955-4DA7-404F-98AE-D0AFF654A0E7', 'currency': 'LINK', 'available_balance': '0', 'total_balance': '0'}, {'id': '90030F73-36FE-4651-9A59-5E57464F64E6', 'currency': 'FTM', 'available_balance': '0', 'total_balance': '0'}, {'id': '44ED1E64-BC45-45DF-B186-2444B8279354', 'currency': 'ZEC', 'available_balance': '0', 'total_balance': '0'}, {'id': '3FD59135-B069-453A-B3F8-F610F9DA7D37', 'currency': 'WGR', 'available_balance': '0', 'total_balance': '0'}, {'id': '441649A4-C8A8-445F-8B91-182B5EC9F20D', 'currency': 'EOS', 'available_balance': '0', 'total_balance': '0'}, {'id': 'FD739CEF-3FB7-4885-9A52-41A0A1C570EA', 'currency': 'NRG', 'available_balance': '0', 'total_balance': '0'}, {'id': '6F0C6353-B145-41A1-B695-2DCF4DFD8ADD', 'currency': 'ETH', 'available_balance': '0', 'total_balance': '0'}, {'id': 'C9C2C810-44AB-48FE-A81F-02B72F9FC87D', 'currency': 'ZRX', 'available_balance': '0', 'total_balance': '0'}, {'id': '6D555386-2091-4C9E-B4FD-7F575F0D9753', 'currency': 'ETC', 'available_balance': '0', 'total_balance': '0'}, {'id': 'A7383484-784E-48AB-8F8A-62304FF7C37A', 'currency': 'USDT', 'available_balance': '0', 'total_balance': '0'}, {'id': '54F5AAB9-3608-46F1-BBA8-F0996FFACE0A', 'currency': 'TOMO', 'available_balance': '0', 'total_balance': '0'}, {'id': '9B2ADE73-CE5D-45EE-9788-3587DDCF58F2', 'currency': 'USDC', 'available_balance': '15.52728', 'total_balance': '15.52728'}, {'id': '974AB4C0-6EDE-4EFF-BCA1-1F8FAEE93AA6', 'currency': 'GUNTHY', 'available_balance': '0', 'total_balance': '0'}, {'id': '8FA31F5A-7A94-4CF6-BA95-9990E9B4C76A', 'currency': 'LTC', 'available_balance': '0', 'total_balance': '0'}, {'id': 'D2CF87AB-754E-4FCD-BB42-7FC812931402', 'currency': 'ICX', 'available_balance': '0', 'total_balance': '0'}, {'id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'currency': 'DASH', 'available_balance': '1', 'total_balance': '1'}, {'id': '35A5F10B-1A9E-47E4-BA27-8918D1A16F8E', 'currency': 'BSV', 'available_balance': '0', 'total_balance': '0'}, {'id': '8A409D82-D049-41C1-A4D2-5F4BA3AF0432', 'currency': 'BCH', 'available_balance': '0', 'total_balance': '0'}, {'id': '0E525961-F6D2-422F-87C1-1A82E325DC8A', 'currency': 'HIVE', 'available_balance': '0', 'total_balance': '0'}, {'id': '03DD4CB5-3BB7-4C74-9FEA-C2CC73123ECE', 'currency': 'XSN', 'available_balance': '0', 'total_balance': '0'}, {'id': 'DA08BE6A-83B7-481A-BD06-5A2BE79E80E6', 'currency': 'GO', 'available_balance': '0', 'total_balance': '0'}, {'id': '81444BA4-EC69-42FE-AD54-09500335788B', 'currency': 'XMR', 'available_balance': '0', 'total_balance': '0'}] + + TRADE_SETTINGS = {'symbols': [{'name': 'HIVEBTC', 'term': 'BTC', 'base': 'HIVE', 'min_size': 1.0, 'max_size': 50000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'XSNBTC', 'term': 'BTC', 'base': 'XSN', 'min_size': 20.0, 'max_size': 17500.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ALEPHETH', 'term': 'ETH', 'base': 'ALEPH', 'min_size': 20.0, 'max_size': 50000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BSVBTC', 'term': 'BTC', 'base': 'BSV', 'min_size': 0.001, 'max_size': 100.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'XRPBTC', 'term': 'BTC', 'base': 'XRP', 'min_size': 0.1, 'max_size': 81632.65, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'NEOBTC', 'term': 'BTC', 'base': 'NEO', 'min_size': 0.01, 'max_size': 1722.65, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'USDTUSDC', 'term': 'USDC', 'base': 'USDT', 'min_size': 10.0, 'max_size': 100000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'GUNTHYBTC', 'term': 'BTC', 'base': 'GUNTHY', 'min_size': 50.0, 'max_size': 50000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ETCBTC', 'term': 'BTC', 'base': 'ETC', 'min_size': 0.01, 'max_size': 2509.41, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BTCUSD', 'term': 'USD', 'base': 'BTC', 'min_size': 0.0005, 'max_size': 25.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BTCUSDT', 'term': 'USDT', 'base': 'BTC', 'min_size': 0.0005, 'max_size': 25.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ZRXBTC', 'term': 'BTC', 'base': 'ZRX', 'min_size': 0.1, 'max_size': 80000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'LINKBTC', 'term': 'BTC', 'base': 'LINK', 'min_size': 1.0, 'max_size': 2500.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BCHBTC', 'term': 'BTC', 'base': 'BCH', 'min_size': 0.001, 'max_size': 60.22, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'LTCBTC', 'term': 'BTC', 'base': 'LTC', 'min_size': 0.01, 'max_size': 310.8, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'EOSBTC', 'term': 'BTC', 'base': 'EOS', 'min_size': 0.01, 'max_size': 5434.78, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ETHUSDC', 'term': 'USDC', 'base': 'ETH', 'min_size': 0.01, 'max_size': 50.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ETHUSD', 'term': 'USD', 'base': 'ETH', 'min_size': 0.001, 'max_size': 25.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ZECBTC', 'term': 'BTC', 'base': 'ZEC', 'min_size': 0.001, 'max_size': 369.21, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BSVUSD', 'term': 'USD', 'base': 'BSV', 'min_size': 0.001, 'max_size': 100.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'DASHBTC', 'term': 'BTC', 'base': 'DASH', 'min_size': 0.01, 'max_size': 218.32, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'XMRBTC', 'term': 'BTC', 'base': 'XMR', 'min_size': 0.001, 'max_size': 277.28, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BXYUSDC', 'term': 'USDC', 'base': 'BXY', 'min_size': 2500.0, 'max_size': 250000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BXYBTC', 'term': 'BTC', 'base': 'BXY', 'min_size': 2500.0, 'max_size': 250000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'GOBTC', 'term': 'BTC', 'base': 'GO', 'min_size': 50.0, 'max_size': 2000000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'AIONBTC', 'term': 'BTC', 'base': 'AION', 'min_size': 0.1, 'max_size': 125000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ICXBTC', 'term': 'BTC', 'base': 'ICX', 'min_size': 0.1, 'max_size': 62500.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'WGRBTC', 'term': 'BTC', 'base': 'WGR', 'min_size': 100.0, 'max_size': 150000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BTCUSDC', 'term': 'USDC', 'base': 'BTC', 'min_size': 0.0005, 'max_size': 25.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'DRGNBTC', 'term': 'BTC', 'base': 'DRGN', 'min_size': 50.0, 'max_size': 1000000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'DRGNUSD', 'term': 'USD', 'base': 'DRGN', 'min_size': 50.0, 'max_size': 200000.0, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'ETHBTC', 'term': 'BTC', 'base': 'ETH', 'min_size': 0.001, 'max_size': 84.65, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BEAMBTC', 'term': 'BTC', 'base': 'BEAM', 'min_size': 0.1, 'max_size': 33333.33, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'WAVESBTC', 'term': 'BTC', 'base': 'WAVES', 'min_size': 0.01, 'max_size': 16528.93, 'maker_fee': 0.15, 'taker_fee': 0.25}, {'name': 'BATBTC', 'term': 'BTC', 'base': 'BAT', 'min_size': 0.1, 'max_size': 90909.09, 'maker_fee': 0.15, 'taker_fee': 0.25}], 'currencies': [{'code': 'LINK', 'name': 'ChainLink', 'precision': 8}, {'code': 'BCH', 'name': 'Bitcoin Cash', 'precision': 8}, {'code': 'EOS', 'name': 'EOS', 'precision': 8}, {'code': 'BSV', 'name': 'BitcoinSV', 'precision': 8}, {'code': 'NRG', 'name': 'Energi', 'precision': 8}, {'code': 'USDT', 'name': 'Tether', 'precision': 8}, {'code': 'ZEC', 'name': 'Zcash', 'precision': 8}, {'code': 'ZRX', 'name': '0x', 'precision': 8}, {'code': 'FTM', 'name': 'Fantom', 'precision': 8}, {'code': 'ICX', 'name': 'ICON', 'precision': 8}, {'code': 'USDC', 'name': 'USD Coin', 'precision': 8}, {'code': 'DASH', 'name': 'Dash', 'precision': 8}, {'code': 'HIVE', 'name': 'Hive', 'precision': 8}, {'code': 'GO', 'name': 'GoChain', 'precision': 8}, {'code': 'BEAM', 'name': 'Beam', 'precision': 8}, {'code': 'AION', 'name': 'Aion', 'precision': 8}, {'code': 'WAVES', 'name': 'Waves', 'precision': 8}, {'code': 'ALEPH', 'name': 'Aleph.im', 'precision': 8}, {'code': 'BTC', 'name': 'Bitcoin', 'precision': 8}, {'code': 'BXY', 'name': 'Beaxy Coin', 'precision': 8}, {'code': 'WGR', 'name': 'Wagerr', 'precision': 8}, {'code': 'USD', 'name': 'United States Dollar', 'precision': 2}, {'code': 'BAT', 'name': 'Basic Attention Token', 'precision': 8}, {'code': 'XSN', 'name': 'Stakenet', 'precision': 8}, {'code': 'XMR', 'name': 'Monero', 'precision': 8}, {'code': 'POLY', 'name': 'Polymath', 'precision': 8}, {'code': 'NEO', 'name': 'Neo', 'precision': 8}, {'code': 'GUNTHY', 'name': 'Gunthy', 'precision': 8}, {'code': 'ETC', 'name': 'Ethereum Classic', 'precision': 8}, {'code': 'VID', 'name': 'VideoCoin', 'precision': 8}, {'code': 'TOMO', 'name': 'TomoChain', 'precision': 8}, {'code': 'DRGN', 'name': 'Dragonchain', 'precision': 8}, {'code': 'LTC', 'name': 'Litecoin', 'precision': 8}, {'code': 'MTL', 'name': 'Metal', 'precision': 8}, {'code': 'XRP', 'name': 'Ripple', 'precision': 8}, {'code': 'ETH', 'name': 'Ethereum', 'precision': 8}]} + + WS_ACCOUNT_BALANCES = [{"account_id": "8FA31F5A-7A94-4CF6-BA95-9990E9B4C76A", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "LTC", "total_statistics": None, "properties": None}, {"account_id": "64BFCB9A-1E19-4A71-AF2E-FE8344F66B56", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "AION", "total_statistics": None, "properties": None}, {"account_id": "6F0C6353-B145-41A1-B695-2DCF4DFD8ADD", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ETH", "total_statistics": [{"total": "0", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}], "properties": None}, {"account_id": "4B4E0955-4DA7-404F-98AE-D0AFF654A0E7", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "LINK", "total_statistics": None, "properties": None}, {"account_id": "2200FC9C-B559-4C70-9605-926E28193605", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "BAT", "total_statistics": [{"total": "0", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}], "properties": None}, {"account_id": "0B0C8512-A113-4D57-B4C6-D059746B302D", "balance": "1.0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "1.0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "1.0", "currency_id": "DASH", "total_statistics": [{"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "deposit"}, {"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "withdrawal"}, {"total": "0.13", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}], "properties": None}, {"account_id": "35A5F10B-1A9E-47E4-BA27-8918D1A16F8E", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "BSV", "total_statistics": None, "properties": None}, {"account_id": "3D0863E2-7AE9-4478-ABED-1FECAF86B639", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ALEPH", "total_statistics": None, "properties": None}, {"account_id": "71AF49AC-54A4-4F5A-A3A1-97CFEFE40430", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "XRP", "total_statistics": None, "properties": None}, {"account_id": "974AB4C0-6EDE-4EFF-BCA1-1F8FAEE93AA6", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "GUNTHY", "total_statistics": None, "properties": None}, {"account_id": "81444BA4-EC69-42FE-AD54-09500335788B", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "XMR", "total_statistics": None, "properties": None}, {"account_id": "C9C2C810-44AB-48FE-A81F-02B72F9FC87D", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ZRX", "total_statistics": None, "properties": None}, {"account_id": "8A409D82-D049-41C1-A4D2-5F4BA3AF0432", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "BCH", "total_statistics": None, "properties": None}, {"account_id": "F801485E-774A-472C-A4D4-CCEEF4AC4C21", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "WAVES", "total_statistics": None, "properties": None}, {"account_id": "03DD4CB5-3BB7-4C74-9FEA-C2CC73123ECE", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "XSN", "total_statistics": None, "properties": None}, {"account_id": "91A87666-6590-45A8-B70F-FA28EFD32716", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "POLY", "total_statistics": None, "properties": None}, {"account_id": "9B2ADE73-CE5D-45EE-9788-3587DDCF58F2", "balance": "28.37140743", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "28.37140743", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "28.37140743", "currency_id": "USDC", "total_statistics": [{"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "deposit"}, {"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "withdrawal"}, {"total": "80", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "credit"}, {"total": "-50.9283871", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}, {"total": "-0.70020547", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "trading_commission"}], "properties": None}, {"account_id": "DA08BE6A-83B7-481A-BD06-5A2BE79E80E6", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "GO", "total_statistics": None, "properties": None}, {"account_id": "53BDA6C6-5C66-4BD7-BE63-D547699525A4", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "DRGN", "total_statistics": None, "properties": None}, {"account_id": "0E525961-F6D2-422F-87C1-1A82E325DC8A", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "HIVE", "total_statistics": None, "properties": None}, {"account_id": "44ED1E64-BC45-45DF-B186-2444B8279354", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ZEC", "total_statistics": None, "properties": None}, {"account_id": "6D555386-2091-4C9E-B4FD-7F575F0D9753", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ETC", "total_statistics": None, "properties": None}, {"account_id": "4C49C9F6-A594-43F1-A351-22C8712596CA", "balance": "0.00060541", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0.00060541", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0.00060541", "currency_id": "BTC", "total_statistics": [{"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "deposit"}, {"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "withdrawal"}, {"total": "0.0007922", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}, {"total": "-0.00018679", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "trading_commission"}], "properties": None}, {"account_id": "A7383484-784E-48AB-8F8A-62304FF7C37A", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "USDT", "total_statistics": None, "properties": None}, {"account_id": "1FB1DD29-406B-4227-AE2A-700D8EDCE001", "balance": "0.0611412", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0.0611412", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0.0611412", "currency_id": "BXY", "total_statistics": [{"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "deposit"}, {"total": "0", "total_this_day": "0", "total_this_week": "0", "total_this_month": "0", "type": "withdrawal"}, {"total": "22500", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "credit"}, {"total": "-22130", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}, {"total": "-369.9388588", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "trading_commission"}], "properties": None}, {"account_id": "FD739CEF-3FB7-4885-9A52-41A0A1C570EA", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "NRG", "total_statistics": None, "properties": None}, {"account_id": "54F5AAB9-3608-46F1-BBA8-F0996FFACE0A", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "TOMO", "total_statistics": None, "properties": None}, {"account_id": "20E33E7E-6940-4521-B3CB-DC75FD9DE0EA", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "NEO", "total_statistics": None, "properties": None}, {"account_id": "3FD59135-B069-453A-B3F8-F610F9DA7D37", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "WGR", "total_statistics": None, "properties": None}, {"account_id": "90030F73-36FE-4651-9A59-5E57464F64E6", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "FTM", "total_statistics": None, "properties": None}, {"account_id": "441649A4-C8A8-445F-8B91-182B5EC9F20D", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "EOS", "total_statistics": [{"total": "0", "total_this_day": None, "total_this_week": None, "total_this_month": None, "type": "execution"}], "properties": None}, {"account_id": "D2CF87AB-754E-4FCD-BB42-7FC812931402", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "ICX", "total_statistics": None, "properties": None}, {"account_id": "B916B506-177A-4359-86F2-80D1845B22B5", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "BEAM", "total_statistics": None, "properties": None}, {"account_id": "D3A33644-A624-4421-BAC6-1F92A93A0CB2", "balance": "0", "unrealized_pnl": None, "realized_pnl": None, "measurement_currency_id": None, "available_for_trading": "0", "enter_average_price": None, "current_price": None, "available_for_withdrawal": "0", "currency_id": "USD", "total_statistics": None, "properties": None}] + + SYMBOLS = [ + { + "symbol": "BXYBTC", + "name": "BXYBTC", + "minimumQuantity": 2500.0, + "maximumQuantity": 250000.0, + "quantityIncrement": 1.0, + "quantityPrecision": 0, + "tickSize": 1e-08, + "baseCurrency": "BXY", + "termCurrency": "BTC", + "pricePrecision": 8, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "DASHBTC", + "name": "DASHBTC", + "minimumQuantity": 0.01, + "maximumQuantity": 218.32, + "quantityIncrement": 1e-05, + "quantityPrecision": 3, + "tickSize": 1e-06, + "baseCurrency": "DASH", + "termCurrency": "BTC", + "pricePrecision": 6, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "ETCBTC", + "name": "ETCBTC", + "minimumQuantity": 0.01, + "maximumQuantity": 2509.41, + "quantityIncrement": 1e-07, + "quantityPrecision": 7, + "tickSize": 1e-07, + "baseCurrency": "ETC", + "termCurrency": "BTC", + "pricePrecision": 7, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "GOBTC", + "name": "GOBTC", + "minimumQuantity": 50.0, + "maximumQuantity": 2000000.0, + "quantityIncrement": 0.001, + "quantityPrecision": 0, + "tickSize": 1e-08, + "baseCurrency": "GO", + "termCurrency": "BTC", + "pricePrecision": 8, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "BTCUSDC", + "name": "BTCUSDC", + "minimumQuantity": 0.001, + "maximumQuantity": 25.0, + "quantityIncrement": 1e-08, + "quantityPrecision": 8, + "tickSize": 0.01, + "baseCurrency": "BTC", + "termCurrency": "USDC", + "pricePrecision": 2, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "ZECBTC", + "name": "ZECBTC", + "minimumQuantity": 0.001, + "maximumQuantity": 369.21, + "quantityIncrement": 1e-05, + "quantityPrecision": 3, + "tickSize": 1e-06, + "baseCurrency": "ZEC", + "termCurrency": "BTC", + "pricePrecision": 6, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "BATBTC", + "name": "BATBTC", + "minimumQuantity": 0.1, + "maximumQuantity": 90909.09, + "quantityIncrement": 0.001, + "quantityPrecision": 6, + "tickSize": 1e-08, + "baseCurrency": "BAT", + "termCurrency": "BTC", + "pricePrecision": 8, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "XRPBTC", + "name": "XRPBTC", + "minimumQuantity": 0.1, + "maximumQuantity": 81632.65, + "quantityIncrement": 0.0001, + "quantityPrecision": 4, + "tickSize": 1e-08, + "baseCurrency": "XRP", + "termCurrency": "BTC", + "pricePrecision": 8, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": True, + }, + { + "symbol": "ALEPHETH", + "name": "ALEPHETH", + "minimumQuantity": 20.0, + "maximumQuantity": 50000.0, + "quantityIncrement": 1.0, + "quantityPrecision": 0, + "tickSize": 1e-08, + "baseCurrency": "ALEPH", + "termCurrency": "ETH", + "pricePrecision": 8, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "crypto", + "suspendedForTrading": False, + }, + { + "symbol": "BTCUSD", + "name": "BTCUSD", + "minimumQuantity": 0.001, + "maximumQuantity": 25.0, + "quantityIncrement": 1e-08, + "quantityPrecision": 8, + "tickSize": 0.01, + "baseCurrency": "BTC", + "termCurrency": "USD", + "pricePrecision": 2, + "buyerTakerCommissionProgressive": 0.25, + "buyerMakerCommissionProgressive": 0.15, + "sellerTakerCommissionProgressive": 0.25, + "sellerMakerCommissionProgressive": 0.15, + "type": "fiat", + "suspendedForTrading": False, + }, + ] + + TRADE_BOOK = { + "type": "SNAPSHOT_FULL_REFRESH", + "security": "DASHBTC", + "timestamp": 1612206339765, + "sequenceNumber": 213148, + "entries": [ + { + "action": "INSERT", + "side": "ASK", + "level": 0, + "numberOfOrders": None, + "quantity": 0.5, + "price": 0.003065, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 1, + "numberOfOrders": None, + "quantity": 1.25, + "price": 0.003066, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 2, + "numberOfOrders": None, + "quantity": 2.5, + "price": 0.003067, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 3, + "numberOfOrders": None, + "quantity": 5.0, + "price": 0.003068, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 4, + "numberOfOrders": None, + "quantity": 7.5, + "price": 0.003069, + }, + { + "action": "INSERT", + "side": "BID", + "level": 0, + "numberOfOrders": None, + "quantity": 0.5, + "price": 0.003053, + }, + { + "action": "INSERT", + "side": "BID", + "level": 1, + "numberOfOrders": None, + "quantity": 1.25, + "price": 0.003052, + }, + { + "action": "INSERT", + "side": "BID", + "level": 2, + "numberOfOrders": None, + "quantity": 2.5, + "price": 0.003051, + }, + { + "action": "INSERT", + "side": "BID", + "level": 3, + "numberOfOrders": None, + "quantity": 5.0, + "price": 0.00305, + }, + { + "action": "INSERT", + "side": "BID", + "level": 4, + "numberOfOrders": None, + "quantity": 7.5, + "price": 0.003049, + }, + ], + } + + EXCHANGE_RATE = { + "ask": 0.003264, + "bid": 0.003256, + "low24": 0.00152, + "high24": 0.0055755, + "volume24": 0.0326, + "change24": -36.43365506483377, + "price": 0.003236, + "volume": 0.01, + "timestamp": 1612345560000, + } + + ACCOUNTS = [ + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ETH", + "id": "6F0C6353-B145-41A1-B695-2DCF4DFD8ADD", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "NRG", + "id": "FD739CEF-3FB7-4885-9A52-41A0A1C570EA", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "NEO", + "id": "20E33E7E-6940-4521-B3CB-DC75FD9DE0EA", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "2500.19579418", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "2500.19579418", + "status": "active", + "available_for_withdrawal": "2500.19579418", + "total_statistics": [ + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "deposit", + }, + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "withdrawal", + }, + { + "total": "7500", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "credit", + }, + { + "total": "-4722", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "execution", + }, + { + "total": "-277.80420582", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "trading_commission", + }, + ], + "currency_id": "BXY", + "id": "1FB1DD29-406B-4227-AE2A-700D8EDCE001", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "AION", + "id": "64BFCB9A-1E19-4A71-AF2E-FE8344F66B56", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "WAVES", + "id": "F801485E-774A-472C-A4D4-CCEEF4AC4C21", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ETC", + "id": "6D555386-2091-4C9E-B4FD-7F575F0D9753", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ZRX", + "id": "C9C2C810-44AB-48FE-A81F-02B72F9FC87D", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "TOMO", + "id": "54F5AAB9-3608-46F1-BBA8-F0996FFACE0A", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "USDT", + "id": "A7383484-784E-48AB-8F8A-62304FF7C37A", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0.00011809", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0.00011809", + "status": "active", + "available_for_withdrawal": "0.00011809", + "total_statistics": [ + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "deposit", + }, + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "withdrawal", + }, + { + "total": "0.00017202", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "execution", + }, + { + "total": "-0.00005393", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "trading_commission", + }, + ], + "currency_id": "BTC", + "id": "4C49C9F6-A594-43F1-A351-22C8712596CA", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "BEAM", + "id": "B916B506-177A-4359-86F2-80D1845B22B5", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "BAT", + "id": "2200FC9C-B559-4C70-9605-926E28193605", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "35.67478", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "35.67478", + "status": "active", + "available_for_withdrawal": "35.67478", + "total_statistics": [ + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "deposit", + }, + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "withdrawal", + }, + { + "total": "80", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "credit", + }, + { + "total": "-44.32522", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "execution", + }, + ], + "currency_id": "USDC", + "id": "9B2ADE73-CE5D-45EE-9788-3587DDCF58F2", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "XRP", + "id": "71AF49AC-54A4-4F5A-A3A1-97CFEFE40430", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "LTC", + "id": "8FA31F5A-7A94-4CF6-BA95-9990E9B4C76A", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "GUNTHY", + "id": "974AB4C0-6EDE-4EFF-BCA1-1F8FAEE93AA6", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ICX", + "id": "D2CF87AB-754E-4FCD-BB42-7FC812931402", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "DRGN", + "id": "53BDA6C6-5C66-4BD7-BE63-D547699525A4", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ALEPH", + "id": "3D0863E2-7AE9-4478-ABED-1FECAF86B639", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "POLY", + "id": "91A87666-6590-45A8-B70F-FA28EFD32716", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0.05", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0.05", + "status": "active", + "available_for_withdrawal": "0.05", + "total_statistics": [ + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "deposit", + }, + { + "total": "0", + "total_this_day": "0", + "total_this_week": "0", + "total_this_month": "0", + "type": "withdrawal", + }, + { + "total": "0.05", + "total_this_day": None, + "total_this_week": None, + "total_this_month": None, + "type": "execution", + }, + ], + "currency_id": "DASH", + "id": "0B0C8512-A113-4D57-B4C6-D059746B302D", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "LINK", + "id": "4B4E0955-4DA7-404F-98AE-D0AFF654A0E7", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "FTM", + "id": "90030F73-36FE-4651-9A59-5E57464F64E6", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "ZEC", + "id": "44ED1E64-BC45-45DF-B186-2444B8279354", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "BSV", + "id": "35A5F10B-1A9E-47E4-BA27-8918D1A16F8E", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "HIVE", + "id": "0E525961-F6D2-422F-87C1-1A82E325DC8A", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "BCH", + "id": "8A409D82-D049-41C1-A4D2-5F4BA3AF0432", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "GO", + "id": "DA08BE6A-83B7-481A-BD06-5A2BE79E80E6", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "XSN", + "id": "03DD4CB5-3BB7-4C74-9FEA-C2CC73123ECE", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "EOS", + "id": "441649A4-C8A8-445F-8B91-182B5EC9F20D", + "properties": None, + }, + { + "measurement_currency_id": None, + "available_for_trading": "0", + "enter_average_price": None, + "current_price": None, + "unrealized_pnl": None, + "realized_pnl": None, + "balance": "0", + "status": "active", + "available_for_withdrawal": "0", + "total_statistics": None, + "currency_id": "XMR", + "id": "81444BA4-EC69-42FE-AD54-09500335788B", + "properties": None, + }, + ] + + HEALTH = {"trading_server": 200, "historical_data_server": 200} + + SNAPSHOT_MSG = { + "type": "SNAPSHOT_FULL_REFRESH", + "security": "DASHBTC", + "timestamp": 1612345351563, + "sequenceNumber": 42820, + "entries": [ + { + "action": "INSERT", + "side": "ASK", + "level": 0, + "numberOfOrders": None, + "quantity": 0.5, + "price": 0.003251, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 1, + "numberOfOrders": None, + "quantity": 1.25, + "price": 0.003252, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 2, + "numberOfOrders": None, + "quantity": 2.5, + "price": 0.003253, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 3, + "numberOfOrders": None, + "quantity": 5.0, + "price": 0.003255, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 4, + "numberOfOrders": None, + "quantity": 7.5, + "price": 0.003256, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 5, + "numberOfOrders": None, + "quantity": 10.0, + "price": 0.003257, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 6, + "numberOfOrders": None, + "quantity": 12.5, + "price": 0.003259, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 7, + "numberOfOrders": None, + "quantity": 20.466, + "price": 0.003264, + }, + { + "action": "INSERT", + "side": "ASK", + "level": 8, + "numberOfOrders": None, + "quantity": 0.5, + "price": 0.00808, + }, + { + "action": "INSERT", + "side": "BID", + "level": 0, + "numberOfOrders": None, + "quantity": 0.4994, + "price": 0.00324, + }, + { + "action": "INSERT", + "side": "BID", + "level": 1, + "numberOfOrders": None, + "quantity": 1.25, + "price": 0.003238, + }, + { + "action": "INSERT", + "side": "BID", + "level": 2, + "numberOfOrders": None, + "quantity": 2.5, + "price": 0.003237, + }, + { + "action": "INSERT", + "side": "BID", + "level": 3, + "numberOfOrders": None, + "quantity": 5.0, + "price": 0.003236, + }, + { + "action": "INSERT", + "side": "BID", + "level": 4, + "numberOfOrders": None, + "quantity": 7.5, + "price": 0.003235, + }, + { + "action": "INSERT", + "side": "BID", + "level": 5, + "numberOfOrders": None, + "quantity": 10.0, + "price": 0.003234, + }, + { + "action": "INSERT", + "side": "BID", + "level": 6, + "numberOfOrders": None, + "quantity": 12.5, + "price": 0.003233, + }, + { + "action": "INSERT", + "side": "BID", + "level": 7, + "numberOfOrders": None, + "quantity": 0.533, + "price": 0.003229, + }, + { + "action": "INSERT", + "side": "BID", + "level": 8, + "numberOfOrders": None, + "quantity": 0.55068, + "price": 0.002249, + }, + { + "action": "INSERT", + "side": "BID", + "level": 9, + "numberOfOrders": None, + "quantity": 134.24942, + "price": 1.4e-5, + }, + { + "action": "INSERT", + "side": "BID", + "level": 10, + "numberOfOrders": None, + "quantity": 60.0, + "price": 1.3e-5, + }, + { + "action": "INSERT", + "side": "BID", + "level": 11, + "numberOfOrders": None, + "quantity": 6.0, + "price": 1.0e-5, + }, + { + "action": "INSERT", + "side": "BID", + "level": 12, + "numberOfOrders": None, + "quantity": 200.0, + "price": 6.0e-6, + }, + ], + } + + OPEN_ORDERS = [{'order_id': '40DF064B-D1C9-4A5E-9C14-C27E34593632', 'symbol': 'BXYBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-BXY-BTC-1613578289001047', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'new', 'size': '3353', 'limit_price': '0.00000021', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-17T16:11:29.3930000Z', 'close_time': None, 'pay_with_utility_token': False, 'post_only': False}] + + CLOSED_ORDERS = [{'order_id': '47BC38D7-A4B4-48D7-A98E-E8E40DE72891', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498528000754', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': '0.005238', 'open_time': '2021-02-16T18:02:08.1980000Z', 'close_time': '2021-02-16T18:02:22.7790000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '6E70B918-D13D-49A5-B4A3-032A4E2C8120', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498601001478', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': '0.005242', 'open_time': '2021-02-16T18:03:21.1600000Z', 'close_time': '2021-02-16T18:03:21.1850000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '72BBF0DE-4B80-438C-8EB1-C1EF51A10B9D', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498601001235', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:03:21.4150000Z', 'close_time': '2021-02-16T18:03:26.3670000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '883AAB4C-3A45-4F4F-A265-232D5E49D4AD', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498528000608', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005228', 'open_time': '2021-02-16T18:02:08.2590000Z', 'close_time': '2021-02-16T18:06:57.4830000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '57B7A379-D77A-4DAA-AA77-074CFAB4339E', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498662001030', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005228', 'open_time': '2021-02-16T18:04:22.1680000Z', 'close_time': '2021-02-16T18:06:57.4850000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '23057BE1-187F-4288-A517-C50884FF4FAA', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498736001136', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005228', 'open_time': '2021-02-16T18:05:36.0030000Z', 'close_time': '2021-02-16T18:06:57.4880000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'D2ACE2E4-4E1D-4B30-9A32-8A34F1EE4613', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498816001458', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005228', 'open_time': '2021-02-16T18:06:56.0310000Z', 'close_time': '2021-02-16T18:06:57.4890000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '234BF40A-120D-4D4E-962C-402653495201', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498878000721', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005217', 'open_time': '2021-02-16T18:07:58.1400000Z', 'close_time': '2021-02-16T18:07:58.1500000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'D0BFFA3F-3E44-47E4-8370-A80BBC593426', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498878000899', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:08:01.5600000Z', 'close_time': '2021-02-16T18:08:03.3850000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E625BAB1-D26F-4CDA-86C6-4809DB76D828', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613498939001837', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005208', 'open_time': '2021-02-16T18:08:59.1630000Z', 'close_time': '2021-02-16T18:08:59.1690000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '7F572842-B33D-43AA-99AA-49A3B2D8DC68', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498939001997', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:08:59.1120000Z', 'close_time': '2021-02-16T18:09:04.2950000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'AF928BB6-B84E-4352-8398-14EF2B0E0D2C', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613499000001648', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005228', 'stop_price': None, 'filled_size': None, 'average_price': '0.005179', 'open_time': '2021-02-16T18:10:00.1530000Z', 'close_time': '2021-02-16T18:10:00.1720000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '4C6AF65F-B1E3-4770-A086-A134E0CD21A6', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613499000001884', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:10:00.1740000Z', 'close_time': '2021-02-16T18:10:05.2920000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9EC5A818-0450-4229-9947-46C83913D45B', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613499417001856', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005219', 'stop_price': None, 'filled_size': None, 'average_price': '0.005219', 'open_time': '2021-02-16T18:16:57.1740000Z', 'close_time': '2021-02-16T18:21:19.5160000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2B5C1601-59EE-47CA-9478-F895144C40D9', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613499491001047', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005219', 'stop_price': None, 'filled_size': None, 'average_price': '0.005219', 'open_time': '2021-02-16T18:18:11.1420000Z', 'close_time': '2021-02-16T18:21:19.5190000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '1D53E604-CB9A-4B34-A61D-AE5D18B5C87E', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613499680001068', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005224', 'stop_price': None, 'filled_size': None, 'average_price': '0.005227', 'open_time': '2021-02-16T18:21:20.1420000Z', 'close_time': '2021-02-16T18:21:20.1730000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '4FB3D3FA-7143-4193-ABB7-BD54FD63153E', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613499680000919', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005213', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:21:20.1750000Z', 'close_time': '2021-02-16T18:21:25.3100000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '0275D285-50E2-4DBE-9A5B-32B91C67639B', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498662001316', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': '0.005238', 'open_time': '2021-02-16T18:04:22.1980000Z', 'close_time': '2021-02-16T18:22:33.2570000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2F562943-529C-45D3-8804-11F0FB334213', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613498736001579', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005238', 'stop_price': None, 'filled_size': None, 'average_price': '0.005238', 'open_time': '2021-02-16T18:05:36.2030000Z', 'close_time': '2021-02-16T18:22:33.2650000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2CD1384E-1557-4082-BCFE-AD3D17CE0FD3', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500261002133', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005244', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:31:10.6720000Z', 'close_time': '2021-02-16T18:31:29.3330000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'B690CAFD-10DF-4B1A-A3D6-29B1E20481BA', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500261001971', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005234', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:31:08.8130000Z', 'close_time': '2021-02-16T18:31:31.0880000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '66763704-7873-4169-A364-59D44F2F5F1F', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500186001467', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005244', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:29:46.1660000Z', 'close_time': '2021-02-16T18:31:32.3590000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '7BAF22E0-C904-419B-8896-59BC34F0F9A0', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500186001254', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005234', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:29:46.1570000Z', 'close_time': '2021-02-16T18:31:33.6310000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'BA80B9E7-353C-44ED-A40F-82D22668B741', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613499491000894', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005208', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:18:11.1830000Z', 'close_time': '2021-02-16T18:31:34.8160000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '310BA54E-8E0C-4D0C-92AD-469EDAC07B6E', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613499417001641', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005208', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:16:57.1570000Z', 'close_time': '2021-02-16T18:31:36.1130000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '904C9D8B-4D9A-4A55-A1F6-F64CE1A2F4F0', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500336001738', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005221', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:32:16.2100000Z', 'close_time': '2021-02-16T18:32:58.2170000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'ED244640-F7AF-44CB-B577-2BCEF01A2DE4', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500336002224', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005232', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:32:16.1630000Z', 'close_time': '2021-02-16T18:32:59.5990000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'AA44C5C4-00F1-45F5-ACA2-A4DFE5D8EDB2', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500436001062', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005208', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:33:56.1730000Z', 'close_time': '2021-02-16T18:34:24.6560000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '38A25D71-B110-43C6-9060-071430352F0D', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500436000888', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005198', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:33:55.7590000Z', 'close_time': '2021-02-16T18:34:25.9860000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '0F5B9334-0EEC-4F29-8128-2CBAB4E15AC1', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500605003800', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005207', 'stop_price': None, 'filled_size': None, 'average_price': '0.005207', 'open_time': '2021-02-16T18:36:45.2350000Z', 'close_time': '2021-02-16T18:40:46.8300000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '7FAE0E46-1F55-4411-86B0-B63B4E48268B', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613500684001365', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005207', 'stop_price': None, 'filled_size': None, 'average_price': '0.005207', 'open_time': '2021-02-16T18:38:03.8960000Z', 'close_time': '2021-02-16T18:40:46.8320000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'CA7498FF-AFF4-4A69-872F-2C8E99DD7122', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500684001215', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005197', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:38:04.1700000Z', 'close_time': '2021-02-16T18:40:52.6270000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'FA36C109-9C70-44FB-8D52-8C4BF297F9D0', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613500605003558', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005197', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:36:45.1410000Z', 'close_time': '2021-02-16T18:40:53.9290000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '80F33E81-43FC-4B34-A386-5BCB91E78A9A', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613501472000799', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005235', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:51:12.0860000Z', 'close_time': '2021-02-16T18:51:17.6990000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'EC3F3D1C-38F0-4A7A-B90C-5E7C706C1C7D', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613501472000934', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005245', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:51:12.1450000Z', 'close_time': '2021-02-16T18:51:17.7060000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2C6261DD-F5CD-4136-86D1-6DF41B32E984', 'symbol': 'BXYBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-BXY-BTC-1613501681001866', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '2500', 'limit_price': '0.00000022', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:54:41.0550000Z', 'close_time': '2021-02-16T18:57:00.4650000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '053E3A5D-4F2F-42C4-B3F6-297F75017815', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613501839001647', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005258', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:57:19.0500000Z', 'close_time': '2021-02-16T18:59:16.7420000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '90C1D9F6-5993-4677-BDB2-8D21E2D7678C', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613501839001952', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00526', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T18:57:19.3690000Z', 'close_time': '2021-02-16T18:59:16.7830000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '60C56397-08A9-438B-B33B-233258D2BA9D', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613502483001655', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005255', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T19:08:09.0510000Z', 'close_time': '2021-02-16T19:08:44.7620000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9B2F1A76-84D0-40B2-B3E0-115DC928350A', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613502483001922', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005265', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T19:08:03.0100000Z', 'close_time': '2021-02-16T19:08:44.7740000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '55088A1D-BD8B-4657-B07D-FAE8E44AC6B6', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613503357001360', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00524', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T19:22:39.1340000Z', 'close_time': '2021-02-16T19:23:12.6180000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'A26A7E86-F7FA-4203-A968-1CF477F0B6AC', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613503393000810', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00522', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T19:23:43.6570000Z', 'close_time': '2021-02-16T19:24:04.5460000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '325141A6-906F-4085-9052-E3ADEABEC25F', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613503445001419', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00522', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T19:24:06.2390000Z', 'close_time': '2021-02-16T19:25:45.5970000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'A68FCAED-42CA-4395-B258-AF249BE5E219', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613509633002402', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005057', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:07:13.2270000Z', 'close_time': '2021-02-16T21:08:54.3440000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'F63638A6-0DCF-4042-8DEF-77C2567D6878', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613509633002594', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005068', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:07:13.0700000Z', 'close_time': '2021-02-16T21:08:54.5220000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '205E0588-0CCA-4180-AD43-3D29BC7F9304', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613509871001581', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005055', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:11:11.0850000Z', 'close_time': '2021-02-16T21:12:10.4420000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '77ABDD20-2418-4789-8655-FF0036DD092A', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613509871001281', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005044', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:11:11.1720000Z', 'close_time': '2021-02-16T21:12:10.5370000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '527BB162-38E0-447C-9222-C21E5BBB6ABF', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613509990001191', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005049', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:13:10.1610000Z', 'close_time': '2021-02-16T21:14:13.6860000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'A07033FD-C6FD-498C-8FFD-85AEE6FA237D', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613509990001443', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:13:10.1680000Z', 'close_time': '2021-02-16T21:14:13.6870000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '82DF808B-7FB0-4929-AE74-E16C56250B39', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510101001709', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005059', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:15:00.9990000Z', 'close_time': '2021-02-16T21:15:35.9550000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '820325F4-F782-4AB2-9F96-4ADDFF659EAE', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510101001850', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005069', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:15:01.3150000Z', 'close_time': '2021-02-16T21:15:35.9560000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '980821A3-7A7D-4D61-A7DB-76B9409460DC', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510302002418', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005063', 'stop_price': None, 'filled_size': None, 'average_price': '0.005063', 'open_time': '2021-02-16T21:18:22.1680000Z', 'close_time': '2021-02-16T21:18:53.3020000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'C2136AB3-4DC9-414F-95EE-9ADFA19B28BB', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510302002689', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005074', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:18:22.1260000Z', 'close_time': '2021-02-16T21:18:57.3530000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'F1D3CF05-80E0-4BB1-958F-3613003855C7', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510401000776', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005027', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:20:01.1540000Z', 'close_time': '2021-02-16T21:21:00.5210000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E14BE50F-C4F3-4304-9CE5-7726D9D49E95', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510401000615', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005017', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:20:01.1700000Z', 'close_time': '2021-02-16T21:21:00.5430000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9A91B273-6F3A-4880-B499-370CF0105145', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510491001927', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': '0.005048', 'open_time': '2021-02-16T21:21:31.0900000Z', 'close_time': '2021-02-16T21:21:57.0120000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '1FD898EB-7C46-4541-8AFC-14D009BE5858', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510491002200', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:21:31.1680000Z', 'close_time': '2021-02-16T21:22:01.2960000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '56E78ECF-B092-40F0-BC3D-0AAAB2C4C76A', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510578001784', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': '0.005048', 'open_time': '2021-02-16T21:22:58.1690000Z', 'close_time': '2021-02-16T21:22:58.1780000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '3FECC3BC-A2AF-45B6-877B-2FCF23EBC811', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510578001934', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:22:58.1720000Z', 'close_time': '2021-02-16T21:23:03.2950000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'D0E08A30-37FC-477D-BD7D-6596F021E93F', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510639000795', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': '0.00504', 'open_time': '2021-02-16T21:23:59.0120000Z', 'close_time': '2021-02-16T21:23:59.1740000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'FA30C9BB-B1E6-4F48-B695-1F93F5031B9A', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510639000972', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:23:59.1740000Z', 'close_time': '2021-02-16T21:24:04.5200000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '594FEC28-FBFB-410E-89B4-9B172EE3BBDB', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510700001330', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': '0.005045', 'open_time': '2021-02-16T21:25:00.1710000Z', 'close_time': '2021-02-16T21:25:00.1770000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '31FD95F9-60E9-4198-8E00-763EFE41BECD', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510700001526', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:25:00.1630000Z', 'close_time': '2021-02-16T21:25:05.2610000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '4716A6CE-2862-4C54-8B0B-B66394D7AD2D', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510761001343', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': '0.005058', 'open_time': '2021-02-16T21:26:01.1810000Z', 'close_time': '2021-02-16T21:26:34.0220000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E6ECB484-2BD3-424F-8995-E4AD62381819', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510761001203', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:26:01.0750000Z', 'close_time': '2021-02-16T21:26:36.5330000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'A1D9CB4F-0D94-49C8-B284-9C24F6F25C87', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510855001470', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': '0.005063', 'open_time': '2021-02-16T21:27:35.1540000Z', 'close_time': '2021-02-16T21:27:35.1670000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '133E522E-B3E3-4CFE-969E-95C25C437700', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510855001314', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:27:35.1130000Z', 'close_time': '2021-02-16T21:27:40.3070000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'CCF02875-BBA1-41FB-84A5-5F4AEAD9FBD6', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613510916001729', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005058', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:28:36.1940000Z', 'close_time': '2021-02-16T21:29:50.4420000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'A64A66DA-8499-4BDE-88B1-EAC6FDF5763C', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613510916001537', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005048', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:28:36.1220000Z', 'close_time': '2021-02-16T21:29:50.4470000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'BEA6F963-1572-4311-A8D1-5F4E536403F1', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511036008230', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005059', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:30:38.0410000Z', 'close_time': '2021-02-16T21:31:01.2790000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2F9933BF-2EE4-433B-BD89-6F9A3A8F4451', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511036008369', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005069', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:30:38.0110000Z', 'close_time': '2021-02-16T21:31:01.3230000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E8E7837F-4C86-496F-BD79-4D4B7C0BF30B', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511128001112', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': '0.005071', 'open_time': '2021-02-16T21:32:08.0250000Z', 'close_time': '2021-02-16T21:32:08.1710000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'C64497F9-24CF-44C4-85EF-7D3C135D3D34', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511128000872', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:32:08.1710000Z', 'close_time': '2021-02-16T21:32:23.3020000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'C46F48AE-6A20-413E-A477-36B03B42960C', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511216000942', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': '0.005075', 'open_time': '2021-02-16T21:33:36.0910000Z', 'close_time': '2021-02-16T21:33:36.1710000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '29ADF01C-23C2-49C3-A002-2EF71587819F', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511216000804', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:33:36.1730000Z', 'close_time': '2021-02-16T21:33:51.2900000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9426B1A1-5420-40EC-BC97-12EEFE3835AE', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511306001806', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': '0.005088', 'open_time': '2021-02-16T21:35:06.1700000Z', 'close_time': '2021-02-16T21:35:06.1820000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2355B1C7-B775-4511-BE30-72F4DAEF0BD5', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511306001579', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:35:06.0350000Z', 'close_time': '2021-02-16T21:35:21.2900000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '738FE5F1-0B50-4312-8296-096FBC2EB909', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511396001639', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': '0.005077', 'open_time': '2021-02-16T21:36:36.1660000Z', 'close_time': '2021-02-16T21:36:36.1730000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'BEE6499F-2778-4668-9684-D353ACF1F2AE', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511396001481', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:36:36.1120000Z', 'close_time': '2021-02-16T21:36:51.2360000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '84EF1C15-249F-4BB4-8728-A2459F30D776', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511486001034', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005044', 'open_time': '2021-02-16T21:38:06.1720000Z', 'close_time': '2021-02-16T21:38:06.2050000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E42E2E8D-0700-4919-916C-44E28CCC61C1', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511486001223', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:38:06.2010000Z', 'close_time': '2021-02-16T21:38:21.4960000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '084ECA44-CA0E-4730-BDA7-E1F0D79F345F', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511561001420', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005057', 'open_time': '2021-02-16T21:39:21.0640000Z', 'close_time': '2021-02-16T21:39:21.1390000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'F463B6EE-318A-4B8D-AA04-483E7E93D83D', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511561001568', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:39:21.1570000Z', 'close_time': '2021-02-16T21:39:36.3680000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '155CCEA9-0FB9-4759-AA9B-218662E05913', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511591001506', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.00506', 'open_time': '2021-02-16T21:39:51.1580000Z', 'close_time': '2021-02-16T21:41:00.9270000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '86805F06-016B-468F-A97B-D764329B8BA4', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511591001722', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:39:51.1780000Z', 'close_time': '2021-02-16T21:41:06.3700000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '3240425B-F32B-4EA3-8928-9BC500A1B6AD', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511681002094', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005057', 'open_time': '2021-02-16T21:41:21.1750000Z', 'close_time': '2021-02-16T21:41:21.1820000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'E32EC58E-C2CB-4479-AFD1-40CCB0A4EBAD', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511681002289', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:41:21.0930000Z', 'close_time': '2021-02-16T21:41:36.3230000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '600279F8-6B1D-4E3F-96C7-C1AFCB913850', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511711016786', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005049', 'open_time': '2021-02-16T21:41:51.1790000Z', 'close_time': '2021-02-16T21:41:51.1850000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9C2F5264-7B70-42B6-8FDA-CDF2E3B812F9', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511711016929', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:41:51.1590000Z', 'close_time': '2021-02-16T21:42:06.3620000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '9DE6C354-1C65-4558-9584-F92D51E3E885', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511741002308', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005047', 'open_time': '2021-02-16T21:42:21.0530000Z', 'close_time': '2021-02-16T21:42:21.1770000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '3D2C9C0A-6555-4D28-B43B-AA6438C9D741', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511741002625', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:42:21.1710000Z', 'close_time': '2021-02-16T21:42:36.2870000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '58D0F179-534D-43CD-92C7-C2D510F885AA', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511771001476', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005046', 'open_time': '2021-02-16T21:42:51.1670000Z', 'close_time': '2021-02-16T21:42:51.1740000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '5552BE4F-28AB-4F0D-B6D1-CF09BA038BAD', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511771001606', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:42:51.1530000Z', 'close_time': '2021-02-16T21:43:06.6120000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'DDFE5FA6-851E-48E1-A724-AA9B809CC76E', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511801001057', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005048', 'open_time': '2021-02-16T21:43:21.0620000Z', 'close_time': '2021-02-16T21:43:21.1720000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': 'FD99E2DF-3DAF-4AE8-8DE4-F71FB66A951E', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511801001199', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:43:21.1660000Z', 'close_time': '2021-02-16T21:43:36.2890000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '31B40871-BD7B-4E3D-A1F0-0BF6BF18A998', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511831001419', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.00506', 'open_time': '2021-02-16T21:43:51.1840000Z', 'close_time': '2021-02-16T21:43:56.0900000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '6EE5CCB5-8D55-4E24-A1ED-DE404791EAD4', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511831001639', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:43:51.1440000Z', 'close_time': '2021-02-16T21:44:06.6060000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '7AF452F3-371D-4282-925C-4BA47B16157F', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511861001287', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.00506', 'open_time': '2021-02-16T21:44:21.1360000Z', 'close_time': '2021-02-16T21:44:49.3090000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '0AB5C307-651D-48E3-B103-CA71ABD352C8', 'symbol': 'DASHBTC', 'wallet_id': '0B0C8512-A113-4D57-B4C6-D059746B302D', 'comment': 'HBOT-sell-DASH-BTC-1613511861001437', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'sell', 'order_status': 'canceled', 'size': '0.01', 'limit_price': '0.005071', 'stop_price': None, 'filled_size': None, 'average_price': None, 'open_time': '2021-02-16T21:44:21.1650000Z', 'close_time': '2021-02-16T21:44:51.2850000Z', 'pay_with_utility_token': False, 'post_only': False}, {'order_id': '2D05D640-5546-448D-BD85-226B51968BBD', 'symbol': 'DASHBTC', 'wallet_id': '4C49C9F6-A594-43F1-A351-22C8712596CA', 'comment': 'HBOT-buy-DASH-BTC-1613511906000617', 'time_in_force': 'good_till_cancel', 'order_type': 'limit', 'side': 'buy', 'order_status': 'completely_filled', 'size': '0.01', 'limit_price': '0.00506', 'stop_price': None, 'filled_size': None, 'average_price': '0.005045', 'open_time': '2021-02-16T21:45:06.0930000Z', 'close_time': '2021-02-16T21:45:06.1710000Z', 'pay_with_utility_token': False, 'post_only': False}] + + ORDERS_OPEN_EMPTY = [] + + ORDERS_CLOSED_EMPTY = [] + + TEST_LIMIT_BUY_ORDER = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_LIMIT_BUY_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user8ea365d0-24a6-83ed-ec03-59bac214fe97\ncontent-type:application/json\nsubscription:sub-humming-1612351104275342\nmessage-id:8ea365d0-24a6-83ed-ec03-59bac214fe97-18\ncontent-length:1397\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"13215876","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.003266","client_order_id":"1D37D726-E162-484B-8816-A43B43549CDD","time_in_force":"gtc","price":"0.003266","expire_time":null,"reason":null,"order_id":"1D37D726-E162-484B-8816-A43B43549CDD","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"limit","order_status":"new","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-8965","type":"submitted","sequence_number":8965,"currency":"DASH","timestamp":1612351106037,"properties":null}],"order":{"average_price":"0","receipt_time":1612351106025,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"1D37D726-E162-484B-8816-A43B43549CDD","timestamp":1612351106037,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.003266","client_order_id":"1D37D726-E162-484B-8816-A43B43549CDD","time_in_force":"gtc","price":"0.003266","expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_LIMIT_BUY_WS_ORDER_COMPLETED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user9d827cdb-52b2-3303-6eba-615683c76ddf\ncontent-type:application/json\nsubscription:sub-humming-1612347898789940\nmessage-id:9d827cdb-52b2-3303-6eba-615683c76ddf-17\ncontent-length:1535\n\n{"events":[{"trade_quantity":"0.01","trade_price":"0.003297","commission":"-0.00000008","original_client_order_id":null,"is_agressor":true,"order_side":"buy","event_id":"0000000000BDBED4","average_price":"0.003297","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.003297","client_order_id":"C715ECA9-D13C-42AE-A978-083F62974B85","time_in_force":"gtc","price":"0.003297","expire_time":null,"reason":null,"order_id":"C715ECA9-D13C-42AE-A978-083F62974B85","cumulative_quantity":"0.01","remaining_quantity":"0","order_type":"limit","order_status":"completely_filled","commission_currency":"BTC","text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-8938","type":"trade","sequence_number":8938,"currency":"DASH","timestamp":1612347915748,"properties":[{"key":"1115","value":"Order"}]}],"order":{"average_price":"0.003297","receipt_time":1612347915627,"close_time":1612347915748,"reason":null,"cumulative_quantity":"0.01","remaining_quantity":"0","status":"completely_filled","id":"C715ECA9-D13C-42AE-A978-083F62974B85","timestamp":1612347915748,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.003297","client_order_id":"C715ECA9-D13C-42AE-A978-083F62974B85","time_in_force":"gtc","price":"0.003297","expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":[{"key":"7224","value":"-0.00000008"}]}}""" + + TEST_LIMIT_SELL_ORDER = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_LIMIT_SELL_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user9cb6b7b3-b47b-97ac-59dd-76e5b33b5953\ncontent-type:application/json\nsubscription:sub-humming-1612367042528182\nmessage-id:9cb6b7b3-b47b-97ac-59dd-76e5b33b5953-20\ncontent-length:1402\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"sell","event_id":"18102008","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.003118","client_order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","time_in_force":"gtc","price":"0.003118","expire_time":null,"reason":null,"order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"limit","order_status":"new","commission_currency":null,"text":"HBOT-sell-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-8998","type":"submitted","sequence_number":8998,"currency":"DASH","timestamp":1612367046019,"properties":null}],"order":{"average_price":"0","receipt_time":1612367046011,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","timestamp":1612367046019,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.003118","client_order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","time_in_force":"gtc","price":"0.003118","expire_time":null,"text":"HBOT-sell-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_LIMIT_SELL_WS_ORDER_COMPLETED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user9cb6b7b3-b47b-97ac-59dd-76e5b33b5953\ncontent-type:application/json\nsubscription:sub-humming-1612367042528182\nmessage-id:9cb6b7b3-b47b-97ac-59dd-76e5b33b5953-21\ncontent-length:1552\n\n{"events":[{"trade_quantity":"0.01","trade_price":"0.003118","commission":"-0.00000008","original_client_order_id":null,"is_agressor":true,"order_side":"sell","event_id":"0000000001143A1A","average_price":"0.003118","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.003118","client_order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","time_in_force":"gtc","price":"0.003118","expire_time":null,"reason":null,"order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","cumulative_quantity":"0.01","remaining_quantity":"0","order_type":"limit","order_status":"completely_filled","commission_currency":"BTC","text":"HBOT-sell-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9004","type":"trade","sequence_number":9004,"currency":"DASH","timestamp":1612367046020,"properties":[{"key":"1115","value":"Order"}]}],"order":{"average_price":"0.003118","receipt_time":1612367046011,"close_time":1612367046020,"reason":null,"cumulative_quantity":"0.01","remaining_quantity":"0","status":"completely_filled","id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","timestamp":1612367046020,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.003118","client_order_id":"E5BA149A-9DD7-4C2E-A2E8-1C37DF6C5393","time_in_force":"gtc","price":"0.003118","expire_time":null,"text":"HBOT-sell-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"limit","source":"CWUI","currency":"DASH","properties":[{"key":"7224","value":"-0.00002565747273893521"}]}}""" + + TEST_UNFILLED_ORDER1 = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_UNFILLED_ORDER2 = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_UNFILLED_ORDER1_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userb0ad4644-7c10-565c-84d0-e1dc576a06f2\ncontent-type:application/json\nsubscription:sub-humming-1612368511313960\nmessage-id:b0ad4644-7c10-565c-84d0-e1dc576a06f2-26\ncontent-length:1397\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"18699442","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.002496","client_order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","time_in_force":"gtc","price":"0.002496","expire_time":null,"reason":null,"order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"limit","order_status":"new","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1612368513006601","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9067","type":"submitted","sequence_number":9067,"currency":"DASH","timestamp":1612368513011,"properties":null}],"order":{"average_price":"0","receipt_time":1612368512949,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"3DE4D10B-6882-4BF8-958A-EA689C145065","timestamp":1612368513011,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.002496","client_order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","time_in_force":"gtc","price":"0.002496","expire_time":null,"text":"HBOT-buy-DASH-BTC-1612368513006601","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_UNFILLED_ORDER2_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userb0ad4644-7c10-565c-84d0-e1dc576a06f2\ncontent-type:application/json\nsubscription:sub-humming-1612368511313960\nmessage-id:b0ad4644-7c10-565c-84d0-e1dc576a06f2-27\ncontent-length:1398\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"sell","event_id":"18699800","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.00311","client_order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","time_in_force":"gtc","price":"0.00311","expire_time":null,"reason":null,"order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"limit","order_status":"new","commission_currency":null,"text":"HBOT-sell-DASH-BTC-1612368514001761","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9078","type":"submitted","sequence_number":9078,"currency":"DASH","timestamp":1612368514021,"properties":null}],"order":{"average_price":"0","receipt_time":1612368513974,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","timestamp":1612368514021,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.00311","client_order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","time_in_force":"gtc","price":"0.00311","expire_time":null,"text":"HBOT-sell-DASH-BTC-1612368514001761","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_UNFILLED_ORDER1_WS_ORDER_CANCELED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userb0ad4644-7c10-565c-84d0-e1dc576a06f2\ncontent-type:application/json\nsubscription:sub-humming-1612368511313960\nmessage-id:b0ad4644-7c10-565c-84d0-e1dc576a06f2-28\ncontent-length:1497\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"18701450","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.002496","client_order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","time_in_force":"gtc","price":"0.002496","expire_time":null,"reason":"Manual cancelation from tradingapi.beaxy.com","order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","cumulative_quantity":"0","remaining_quantity":"0","order_type":"limit","order_status":"canceled","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1612368513006601","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9089","type":"cancel","sequence_number":9089,"currency":"DASH","timestamp":1612368517010,"properties":null}],"order":{"average_price":"0","receipt_time":1612368512949,"close_time":1612368517010,"reason":"Manual cancelation from tradingapi.beaxy.com","cumulative_quantity":"0","remaining_quantity":"0.01","status":"canceled","id":"3DE4D10B-6882-4BF8-958A-EA689C145065","timestamp":1612368517010,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.002496","client_order_id":"3DE4D10B-6882-4BF8-958A-EA689C145065","time_in_force":"gtc","price":"0.002496","expire_time":null,"text":"HBOT-buy-DASH-BTC-1612368513006601","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_UNFILLED_ORDER2_WS_ORDER_CANCELED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userb0ad4644-7c10-565c-84d0-e1dc576a06f2\ncontent-type:application/json\nsubscription:sub-humming-1612368511313960\nmessage-id:b0ad4644-7c10-565c-84d0-e1dc576a06f2-29\ncontent-length:1498\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"sell","event_id":"18701522","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.00311","client_order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","time_in_force":"gtc","price":"0.00311","expire_time":null,"reason":"Manual cancelation from tradingapi.beaxy.com","order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","cumulative_quantity":"0","remaining_quantity":"0","order_type":"limit","order_status":"canceled","commission_currency":null,"text":"HBOT-sell-DASH-BTC-1612368514001761","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9100","type":"cancel","sequence_number":9100,"currency":"DASH","timestamp":1612368517226,"properties":null}],"order":{"average_price":"0","receipt_time":1612368513974,"close_time":1612368517226,"reason":"Manual cancelation from tradingapi.beaxy.com","cumulative_quantity":"0","remaining_quantity":"0.01","status":"canceled","id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","timestamp":1612368517226,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.00311","client_order_id":"9C4B5661-7C66-4AEF-AD5D-F6E813228F2A","time_in_force":"gtc","price":"0.00311","expire_time":null,"text":"HBOT-sell-DASH-BTC-1612368514001761","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + + TEST_MARKET_BUY_ORDER = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_MARKET_BUY_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userc2959dab-c524-e54c-b973-1aec61426cce\ncontent-type:application/json\nsubscription:sub-humming-1612371211987894\nmessage-id:c2959dab-c524-e54c-b973-1aec61426cce-35\ncontent-length:1375\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"19742404","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"market","order_status":"new","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9229","type":"submitted","sequence_number":9229,"currency":"DASH","timestamp":1612371214001,"properties":null}],"order":{"average_price":"0","receipt_time":1612371213978,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","timestamp":1612371214001,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"market","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_MARKET_BUY_WS_ORDER_COMPLETED = """MESSAGE\nstatus:200\ndestination:/v1/orders-userc2959dab-c524-e54c-b973-1aec61426cce\ncontent-type:application/json\nsubscription:sub-humming-1612371211987894\nmessage-id:c2959dab-c524-e54c-b973-1aec61426cce-36\ncontent-length:1513\n\n{"events":[{"trade_quantity":"0.01","trade_price":"0.003075","commission":"-0.00000008","original_client_order_id":null,"is_agressor":true,"order_side":"buy","event_id":"00000000012D41E6","average_price":"0.003075","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","cumulative_quantity":"0.01","remaining_quantity":"0","order_type":"market","order_status":"completely_filled","commission_currency":"BTC","text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9235","type":"trade","sequence_number":9235,"currency":"DASH","timestamp":1612371214001,"properties":[{"key":"1115","value":"Order"}]}],"order":{"average_price":"0.003075","receipt_time":1612371213978,"close_time":1612371214001,"reason":null,"cumulative_quantity":"0.01","remaining_quantity":"0","status":"completely_filled","id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","timestamp":1612371214001,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"E7444777-4AAC-43D9-9AF2-61BE027D42ED","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"market","source":"CWUI","currency":"DASH","properties":[{"key":"7224","value":"-0.00000008"}]}}""" + + TEST_MARKET_SELL_ORDER = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_MARKET_SELL_WS_ORDER_CREATED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user178e98a8-ccf2-0706-1d57-e30f5e9b91ba\ncontent-type:application/json\nsubscription:sub-humming-1612371349761545\nmessage-id:178e98a8-ccf2-0706-1d57-e30f5e9b91ba-37\ncontent-length:1380\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"sell","event_id":"19803703","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"market","order_status":"new","commission_currency":null,"text":"HBOT-sell-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9262","type":"submitted","sequence_number":9262,"currency":"DASH","timestamp":1612371349999,"properties":null}],"order":{"average_price":"0","receipt_time":1612371349955,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","timestamp":1612371349999,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-sell-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"market","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_MARKET_SELL_WS_ORDER_COMPLETED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user178e98a8-ccf2-0706-1d57-e30f5e9b91ba\ncontent-type:application/json\nsubscription:sub-humming-1612371349761545\nmessage-id:178e98a8-ccf2-0706-1d57-e30f5e9b91ba-38\ncontent-length:1530\n\n{"events":[{"trade_quantity":"0.01","trade_price":"0.003057","commission":"-0.00000008","original_client_order_id":null,"is_agressor":true,"order_side":"sell","event_id":"00000000012E3159","average_price":"0.003057","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","cumulative_quantity":"0.01","remaining_quantity":"0","order_type":"market","order_status":"completely_filled","commission_currency":"BTC","text":"HBOT-sell-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9268","type":"trade","sequence_number":9268,"currency":"DASH","timestamp":1612371350000,"properties":[{"key":"1115","value":"Order"}]}],"order":{"average_price":"0.003057","receipt_time":1612371349955,"close_time":1612371350000,"reason":null,"cumulative_quantity":"0.01","remaining_quantity":"0","status":"completely_filled","id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","timestamp":1612371350000,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"770AE260-895C-41DF-A5E9-E67904E7F8C0","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-sell-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"market","source":"CWUI","currency":"DASH","properties":[{"key":"7224","value":"-0.00002616944717042852"}]}}""" + + TEST_CANCEL_BUY_ORDER = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_CANCEL_BUY_WS_ORDER_COMPLETED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user1b8f7a06-858e-c0dc-8665-a99fb892c363\ncontent-type:application/json\nsubscription:sub-humming-1612373137250051\nmessage-id:1b8f7a06-858e-c0dc-8665-a99fb892c363-45\ncontent-length:1397\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"20424624","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.001544","client_order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","time_in_force":"gtc","price":"0.001544","expire_time":null,"reason":null,"order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"limit","order_status":"new","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9389","type":"submitted","sequence_number":9389,"currency":"DASH","timestamp":1612373138034,"properties":null}],"order":{"average_price":"0","receipt_time":1612373137964,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"C24200B6-A16B-46C6-88A2-6D348671B25E","timestamp":1612373138034,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.001544","client_order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","time_in_force":"gtc","price":"0.001544","expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_CANCEL_BUY_WS_ORDER_CANCELED = """MESSAGE\nstatus:200\ndestination:/v1/orders-user1b8f7a06-858e-c0dc-8665-a99fb892c363\ncontent-type:application/json\nsubscription:sub-humming-1612373137250051\nmessage-id:1b8f7a06-858e-c0dc-8665-a99fb892c363-46\ncontent-length:1497\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"buy","event_id":"20425042","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":"0.001544","client_order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","time_in_force":"gtc","price":"0.001544","expire_time":null,"reason":"Manual cancelation from tradingapi.beaxy.com","order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","cumulative_quantity":"0","remaining_quantity":"0","order_type":"limit","order_status":"canceled","commission_currency":null,"text":"HBOT-buy-DASH-BTC-1","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"BTC-9400","type":"cancel","sequence_number":9400,"currency":"DASH","timestamp":1612373139033,"properties":null}],"order":{"average_price":"0","receipt_time":1612373137964,"close_time":1612373139033,"reason":"Manual cancelation from tradingapi.beaxy.com","cumulative_quantity":"0","remaining_quantity":"0.01","status":"canceled","id":"C24200B6-A16B-46C6-88A2-6D348671B25E","timestamp":1612373139033,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":"0.001544","client_order_id":"C24200B6-A16B-46C6-88A2-6D348671B25E","time_in_force":"gtc","price":"0.001544","expire_time":null,"text":"HBOT-buy-DASH-BTC-1","destination":"MAXI","security_id":"DASHBTC","side":"buy","type":"limit","source":"CWUI","currency":"DASH","properties":null}}""" + + TEST_CANCEL_ALL_ORDER1 = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_CANCEL_ALL_ORDER2 = {'order_id': '435118B0-A7F7-40D2-A409-820E8FC342A2'} + TEST_CANCEL_BUY_WS_ORDER1_CANCELED = """MESSAGE\nstatus:200\ndestination:/v1/orders-usere53ecdd6-383c-ef5d-1d1f-5471624e0ad9\ncontent-type:application/json\nsubscription:sub-humming-1612372490957013\nmessage-id:e53ecdd6-383c-ef5d-1d1f-5471624e0ad9-43\ncontent-length:1380\n\n{"events":[{"trade_quantity":null,"trade_price":null,"commission":null,"original_client_order_id":null,"is_agressor":null,"order_side":"sell","event_id":"20206349","average_price":"0","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","cumulative_quantity":"0","remaining_quantity":"0.01","order_type":"market","order_status":"new","commission_currency":null,"text":"HBOT-sell-DASH-BTC-1612372497005441","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9356","type":"submitted","sequence_number":9356,"currency":"DASH","timestamp":1612372497234,"properties":null}],"order":{"average_price":"0","receipt_time":1612372497184,"close_time":0,"reason":null,"cumulative_quantity":"0","remaining_quantity":"0.01","status":"new","id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","timestamp":1612372497234,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-sell-DASH-BTC-1612372497005441","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"market","source":"CWUI","currency":"DASH","properties":null}}""" + TEST_CANCEL_BUY_WS_ORDER2_CANCELED = """MESSAGE\nstatus:200\ndestination:/v1/orders-usere53ecdd6-383c-ef5d-1d1f-5471624e0ad9\ncontent-type:application/json\nsubscription:sub-humming-1612372490957013\nmessage-id:e53ecdd6-383c-ef5d-1d1f-5471624e0ad9-44\ncontent-length:1530\n\n{"events":[{"trade_quantity":"0.01","trade_price":"0.003068","commission":"-0.00000008","original_client_order_id":null,"is_agressor":true,"order_side":"sell","event_id":"000000000134562F","average_price":"0.003068","stop_price":null,"leverage":null,"quantity":"0.01","limit_price":null,"client_order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","time_in_force":"ioc","price":null,"expire_time":null,"reason":null,"order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","cumulative_quantity":"0.01","remaining_quantity":"0","order_type":"market","order_status":"completely_filled","commission_currency":"BTC","text":"HBOT-sell-DASH-BTC-1612372497005441","exchange_id":null,"destination":"MAXI","security_id":"DASHBTC","id":"DASH-9362","type":"trade","sequence_number":9362,"currency":"DASH","timestamp":1612372497236,"properties":[{"key":"1115","value":"Order"}]}],"order":{"average_price":"0.003068","receipt_time":1612372497184,"close_time":1612372497236,"reason":null,"cumulative_quantity":"0.01","remaining_quantity":"0","status":"completely_filled","id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","timestamp":1612372497236,"stop_price":null,"leverage":null,"submission_time":null,"quantity":"0.01","limit_price":null,"client_order_id":"5484A8B3-8C06-4C83-AFC3-B01EB500AD09","time_in_force":"ioc","price":null,"expire_time":null,"text":"HBOT-sell-DASH-BTC-1612372497005441","destination":"MAXI","security_id":"DASHBTC","side":"sell","type":"market","source":"CWUI","currency":"DASH","properties":[{"key":"7224","value":"-0.00002607561929595828"}]}}""" diff --git a/test/integration/test_beaxy_active_order_tracker.py b/test/integration/test_beaxy_active_order_tracker.py new file mode 100644 index 0000000000..3c1637cc30 --- /dev/null +++ b/test/integration/test_beaxy_active_order_tracker.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python + +from os.path import join, realpath +import sys; sys.path.insert(0, realpath(join(__file__, "../../../"))) + +import unittest +from typing import Any, Dict + +from hummingbot.core.data_type.order_book_row import OrderBookRow +from hummingbot.connector.exchange.beaxy.beaxy_order_book_message import BeaxyOrderBookMessage +from hummingbot.connector.exchange.beaxy.beaxy_order_book import BeaxyOrderBook +from hummingbot.connector.exchange.beaxy.beaxy_active_order_tracker import BeaxyActiveOrderTracker + +from test.integration.assets.mock_data.fixture_beaxy import FixtureBeaxy + + +test_trading_pair = "BTC-USDC" + + +class BeaxyOrderBookTrackerUnitTest(unittest.TestCase): + def test_insert_update_delete_messages(self): + active_tracker = BeaxyActiveOrderTracker() + + # receive INSERT message to be added to active orders + side = "BID" + price = 1337.4423423404 + quantity: float = 1 + update_id = 123 + message_dict: Dict[str, Any] = { + 'timestamp': 1, + 'sequenceNumber': update_id, + 'entries': [{ + "action": "INSERT", + "quantity": quantity, + "price": price, + "side": side, + "sequrity": test_trading_pair + }] + } + insert_message = BeaxyOrderBook.diff_message_from_exchange(message_dict, float(12345)) + insert_ob_row: OrderBookRow = active_tracker.convert_diff_message_to_order_book_row(insert_message) + self.assertEqual(insert_ob_row[0], [OrderBookRow(price, quantity, update_id)]) + + # receive UPDATE message + updated_quantity: float = 3.2 + update_message_dict: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": update_id + 1, + 'entries': [{ + "action": "UPDATE", + "quantity": updated_quantity, + "price": price, + "side": side, + "sequrity": test_trading_pair + }] + } + change_message = BeaxyOrderBook.diff_message_from_exchange(update_message_dict, float(12345)) + change_ob_row: OrderBookRow = active_tracker.convert_diff_message_to_order_book_row(change_message) + self.assertEqual(change_ob_row[0], [OrderBookRow(price, float(updated_quantity), update_id + 1)]) + + # receive DELETE message + delete_quantity = 1 + delete_message_dict: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": update_id + 2, + 'entries': [{ + "action": "DELETE", + "quantity": delete_quantity, + "price": price, + "side": side, + "sequrity": test_trading_pair + }] + } + + delete_message: BeaxyOrderBookMessage = BeaxyOrderBook.diff_message_from_exchange(delete_message_dict, float(12345)) + delete_ob_row: OrderBookRow = active_tracker.convert_diff_message_to_order_book_row(delete_message) + self.assertEqual(delete_ob_row[0], [OrderBookRow(price, float(0), update_id + 1 + 1)]) + + def test_delete_through(self): + active_tracker = BeaxyActiveOrderTracker() + + # receive INSERT message to be added to active orders + first_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "INSERT", + "quantity": 1, + "price": 133, + "side": "BID", + "sequrity": test_trading_pair, + }] + } + second_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 2, + 'entries': [{ + "action": "INSERT", + "quantity": 2, + "price": 134, + "side": "BID", + "sequrity": test_trading_pair + }] + } + third_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "INSERT", + "quantity": 3, + "price": 135, + "side": "BID", + "sequrity": test_trading_pair + }] + } + + inserts = [first_insert, second_insert, third_insert] + for msg in inserts: + insert_message = BeaxyOrderBook.diff_message_from_exchange(msg, float(12345)) + active_tracker.convert_diff_message_to_order_book_row(insert_message) + + delete_through_dict: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "DELETE_THROUGH", + "quantity": 3, + "price": 134, + "side": "BID", + "sequrity": test_trading_pair + }] + } + + msg = BeaxyOrderBook.diff_message_from_exchange(delete_through_dict, float(12345)) + active_tracker.convert_diff_message_to_order_book_row(msg) + self.assertEqual(len(active_tracker.active_bids), 2) + self.assertEqual(next(iter(active_tracker.active_bids)), 134) + + def test_delete_from(self): + active_tracker = BeaxyActiveOrderTracker() + + # receive INSERT message to be added to active orders + first_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "INSERT", + "quantity": 1, + "price": 133, + "side": "ASK", + "sequrity": test_trading_pair + }] + } + second_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 2, + 'entries': [{ + "action": "INSERT", + "quantity": 2, + "price": 134, + "side": "ASK", + "sequrity": test_trading_pair + }] + } + third_insert: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "INSERT", + "quantity": 3, + "price": 135, + "side": "ASK", + "sequrity": test_trading_pair + }] + } + + inserts = [first_insert, second_insert, third_insert] + for msg in inserts: + insert_message = BeaxyOrderBook.diff_message_from_exchange(msg, float(12345)) + active_tracker.convert_diff_message_to_order_book_row(insert_message) + + delete_through_dict: Dict[str, Any] = { + 'timestamp': 1, + "sequenceNumber": 1, + 'entries': [{ + "action": "DELETE_FROM", + "quantity": 3, + "price": 134, + "side": "ASK", + "sequrity": test_trading_pair + }] + } + + msg = BeaxyOrderBook.diff_message_from_exchange(delete_through_dict, float(12345)) + active_tracker.convert_diff_message_to_order_book_row(msg) + self.assertEqual(len(active_tracker.active_asks), 2) + self.assertEqual(next(iter(active_tracker.active_asks)), 133) + + def test_snapshot(self): + active_tracker = BeaxyActiveOrderTracker() + insert_message = BeaxyOrderBook.snapshot_message_from_exchange(FixtureBeaxy.SNAPSHOT_MSG, float(12345)) + + active_tracker.convert_snapshot_message_to_order_book_row(insert_message) + + self.assertEqual(len(active_tracker.active_asks), 9) diff --git a/test/integration/test_beaxy_api_order_book_data_source.py b/test/integration/test_beaxy_api_order_book_data_source.py new file mode 100644 index 0000000000..2006d936e7 --- /dev/null +++ b/test/integration/test_beaxy_api_order_book_data_source.py @@ -0,0 +1,72 @@ +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth +import unittest +import conf +import asyncio +import aiohttp +import pandas as pd +from typing import ( + List, + Optional, + Any, + Dict +) +from hummingbot.connector.exchange.beaxy.beaxy_api_order_book_data_source import BeaxyAPIOrderBookDataSource +from hummingbot.core.data_type.order_book_tracker_entry import OrderBookTrackerEntry + + +class BeaxyApiOrderBookDataSourceUnitTest(unittest.TestCase): + + trading_pairs: List[str] = [ + "BTC-USDC", + ] + + @classmethod + def setUpClass(cls): + cls.ev_loop = asyncio.get_event_loop() + cls._auth: BeaxyAuth = BeaxyAuth(conf.beaxy_api_key, conf.beaxy_secret_key) + cls.data_source: BeaxyAPIOrderBookDataSource = BeaxyAPIOrderBookDataSource(cls.trading_pairs) + + def run_async(self, task): + return self.ev_loop.run_until_complete(task) + + def test_get_active_exchange_markets(self): + all_markets_df: pd.DataFrame = self.run_async(self.data_source.get_active_exchange_markets()) + + # Check DF type + self.assertIsInstance(all_markets_df, pd.DataFrame) + + # Check DF order, make sure it's sorted by USDVolume col in desending order + usd_volumes = all_markets_df.loc[:, 'USDVolume'].to_list() + self.assertListEqual( + usd_volumes, + sorted(usd_volumes, reverse=True), + "The output usd volumes should remain the same after being sorted again") + + def test_get_trading_pairs(self): + trading_pairs: Optional[List[str]] = self.run_async(self.data_source.get_trading_pairs()) + assert trading_pairs is not None + self.assertIn("ETC-BTC", trading_pairs) + self.assertNotIn("NRG-BTC", trading_pairs) + + async def get_snapshot(self): + async with aiohttp.ClientSession() as client: + trading_pairs: Optional[List[str]] = await self.data_source.get_trading_pairs() + assert trading_pairs is not None + trading_pair: str = trading_pairs[0] + try: + snapshot: Dict[str, Any] = await self.data_source.get_snapshot(client, trading_pair, 20) + return snapshot + except Exception: + return None + + def test_get_snapshot(self): + snapshot: Optional[Dict[str, Any]] = self.run_async(self.get_snapshot()) + assert snapshot is not None + self.assertIsNotNone(snapshot) + trading_pairs = self.run_async(self.data_source.get_trading_pairs()) + assert trading_pairs is not None + self.assertIn(snapshot["security"], [p.replace('-', '') for p in trading_pairs]) + + def test_get_tracking_pairs(self): + tracking_pairs: Dict[str, OrderBookTrackerEntry] = self.run_async(self.data_source.get_tracking_pairs()) + self.assertIsInstance(tracking_pairs["BTC-USDC"], OrderBookTrackerEntry) diff --git a/test/integration/test_beaxy_auth.py b/test/integration/test_beaxy_auth.py new file mode 100644 index 0000000000..2024dcee9a --- /dev/null +++ b/test/integration/test_beaxy_auth.py @@ -0,0 +1,18 @@ +import unittest +import asyncio +import conf +from typing import ( + Dict +) +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth + + +class BeaxyAuthUnitTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.ev_loop = asyncio.get_event_loop() + cls.beaxy_auth: BeaxyAuth = BeaxyAuth(conf.beaxy_api_key, conf.beaxy_secret_key) + + def test_get_auth_session(self): + result: Dict[str, str] = self.ev_loop.run_until_complete(self.beaxy_auth.generate_auth_dict("GET", "/api/staff")) + self.assertIsNotNone(result) diff --git a/test/integration/test_beaxy_market.py b/test/integration/test_beaxy_market.py new file mode 100644 index 0000000000..0e6cfc1799 --- /dev/null +++ b/test/integration/test_beaxy_market.py @@ -0,0 +1,536 @@ +import os +from os.path import join, realpath +import sys; sys.path.insert(0, realpath(join(__file__, "../../../"))) +import logging +from hummingbot.logger.struct_logger import METRICS_LOG_LEVEL +import asyncio +import json +import contextlib +import time +import unittest +from unittest import mock +import conf +from decimal import Decimal +from hummingbot.core.clock import ( + Clock, + ClockMode +) +from hummingbot.core.utils.async_utils import ( + safe_ensure_future, + safe_gather, +) +from hummingbot.client.config.fee_overrides_config_map import fee_overrides_config_map +from test.integration.humming_web_app import HummingWebApp +from test.integration.humming_ws_server import HummingWsServerFactory +from hummingbot.market.market_base import OrderType +from hummingbot.core.event.event_logger import EventLogger +from hummingbot.core.event.events import ( + MarketEvent, SellOrderCreatedEvent, + TradeFee, + TradeType, + BuyOrderCompletedEvent, + OrderFilledEvent, + OrderCancelledEvent, + BuyOrderCreatedEvent, + SellOrderCompletedEvent, + MarketOrderFailureEvent, +) +from hummingbot.connector.exchange.beaxy.beaxy_exchange import BeaxyExchange +from hummingbot.connector.exchange.beaxy.beaxy_constants import BeaxyConstants +from typing import ( + List, +) +from test.integration.assets.mock_data.fixture_beaxy import FixtureBeaxy + +logging.basicConfig(level=METRICS_LOG_LEVEL) +API_MOCK_ENABLED = conf.mock_api_enabled is not None and conf.mock_api_enabled.lower() in ['true', 'yes', '1'] +API_KEY = "XXX" if API_MOCK_ENABLED else conf.beaxy_api_key +API_SECRET = "YYY" if API_MOCK_ENABLED else conf.beaxy_secret_key + + +def _transform_raw_message_patch(self, msg): + return json.loads(msg) + + +PUBLIC_API_BASE_URL = "services.beaxy.com" +PRIVET_API_BASE_URL = "tradewith.beaxy.com" + + +class BeaxyExchangeUnitTest(unittest.TestCase): + events: List[MarketEvent] = [ + MarketEvent.ReceivedAsset, + MarketEvent.BuyOrderCompleted, + MarketEvent.SellOrderCompleted, + MarketEvent.WithdrawAsset, + MarketEvent.OrderFilled, + MarketEvent.TransactionFailure, + MarketEvent.BuyOrderCreated, + MarketEvent.SellOrderCreated, + MarketEvent.OrderCancelled, + MarketEvent.OrderFailure, + ] + market: BeaxyExchange + market_logger: EventLogger + stack: contextlib.ExitStack + + @classmethod + def setUpClass(cls): + + cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() + + if API_MOCK_ENABLED: + + cls.web_app = HummingWebApp.get_instance() + cls.web_app.add_host_to_mock(PRIVET_API_BASE_URL, []) + cls.web_app.add_host_to_mock(PUBLIC_API_BASE_URL, []) + cls.web_app.start() + cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) + cls._patcher = mock.patch("aiohttp.client.URL") + cls._url_mock = cls._patcher.start() + cls._url_mock.side_effect = cls.web_app.reroute_local + cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols", FixtureBeaxy.SYMBOLS) + cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols/DASHBTC/book", + FixtureBeaxy.TRADE_BOOK) + cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols/DASHBTC/rate", + FixtureBeaxy.EXCHANGE_RATE) + cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/health", + FixtureBeaxy.HEALTH) + cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/wallets", + FixtureBeaxy.BALANCES) + cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/tradingsettings", + FixtureBeaxy.TRADE_SETTINGS) + cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/orders/open", + FixtureBeaxy.ORDERS_OPEN_EMPTY) + cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/orders/closed", + FixtureBeaxy.ORDERS_CLOSED_EMPTY) + + cls._t_nonce_patcher = unittest.mock.patch( + "hummingbot.connector.exchange.beaxy.beaxy_exchange.get_tracking_nonce") + cls._t_nonce_mock = cls._t_nonce_patcher.start() + + HummingWsServerFactory.url_host_only = True + HummingWsServerFactory.start_new_server(BeaxyConstants.TradingApi.WS_BASE_URL) + HummingWsServerFactory.start_new_server(BeaxyConstants.PublicApi.WS_BASE_URL) + + cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) + cls._ws_mock = cls._ws_patcher.start() + cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect + + cls._auth_confirm_patcher = unittest.mock.patch( + "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__login_confirm") + cls._auth_confirm_mock = cls._auth_confirm_patcher.start() + cls._auth_session_patcher = unittest.mock.patch( + "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__get_session_data") + cls._auth_session_mock = cls._auth_session_patcher.start() + cls._auth_session_mock.return_value = {"sign_key": 123, "session_id": '123'} + cls._auth_headers_patcher = unittest.mock.patch( + "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth.get_token") + cls._auth_headers_mock = cls._auth_headers_patcher.start() + cls._auth_headers_mock.return_value = '123' + cls._auth_poll_patcher = unittest.mock.patch( + "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._auth_token_polling_loop") + cls._auth_poll_mock = cls._auth_poll_patcher.start() + + cls.clock: Clock = Clock(ClockMode.REALTIME) + cls.market: BeaxyExchange = BeaxyExchange( + API_KEY, API_SECRET, + trading_pairs=["DASH-BTC"] + ) + + if API_MOCK_ENABLED: + async def mock_status_polling_task(): + pass + + # disable status polling as it will make orders update inconsistent from mock view + cls.market._status_polling_task = asyncio.ensure_future(mock_status_polling_task()) + cls.ev_loop.run_until_complete(cls.market._update_balances()) + + print("Initializing Beaxy market... this will take about a minute.") + cls.clock.add_iterator(cls.market) + cls.stack: contextlib.ExitStack = contextlib.ExitStack() + cls._clock = cls.stack.enter_context(cls.clock) + cls.ev_loop.run_until_complete(cls.wait_til_ready()) + print("Ready.") + + @classmethod + async def wait_til_ready(cls): + while True: + now = time.time() + next_iteration = now // 1.0 + 1 + if cls.market.ready: + break + else: + await cls._clock.run_til(next_iteration) + await asyncio.sleep(1.0) + + def run_parallel(self, *tasks): + return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) + + async def run_parallel_async(self, *tasks): + future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) + while not future.done(): + now = time.time() + next_iteration = now // 1.0 + 1 + await self.clock.run_til(next_iteration) + return future.result() + + def setUp(self): + self.db_path: str = realpath(join(__file__, "../beaxy_test.sqlite")) + try: + os.unlink(self.db_path) + except FileNotFoundError: + pass + + self.market_logger = EventLogger() + for event_tag in self.events: + self.market.add_listener(event_tag, self.market_logger) + + def test_balances(self): + balances = self.market.get_all_balances() + self.assertGreater(len(balances), 0) + + def test_get_fee(self): + limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) + self.assertGreater(limit_fee.percent, 0) + self.assertEqual(len(limit_fee.flat_fees), 0) + market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) + self.assertGreater(market_fee.percent, 0) + self.assertEqual(len(market_fee.flat_fees), 0) + + def test_fee_overrides_config(self): + fee_overrides_config_map["beaxy_taker_fee"].value = None + taker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), + Decimal('0.1')) + self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent) + fee_overrides_config_map["beaxy_taker_fee"].value = Decimal('0.2') + taker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), + Decimal('0.1')) + self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) + fee_overrides_config_map["beaxy_maker_fee"].value = None + maker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), + Decimal('0.1')) + self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent) + fee_overrides_config_map["beaxy_maker_fee"].value = Decimal('0.75') + maker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), + Decimal('0.1')) + self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent) + + def place_order(self, is_buy, trading_pair, amount, order_type, price, ws_resps=[]): + global EXCHANGE_ORDER_ID + order_id, exch_order_id = None, None + + if is_buy: + order_id = self.market.buy(trading_pair, amount, order_type, price) + else: + order_id = self.market.sell(trading_pair, amount, order_type, price) + if API_MOCK_ENABLED: + for delay, ws_resp in ws_resps: + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, ws_resp, delay=delay) + return order_id, exch_order_id + + def cancel_order(self, trading_pair, order_id, exch_order_id): + self.market.cancel(trading_pair, order_id) + + def test_limit_buy(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_LIMIT_BUY_ORDER) + + amount: Decimal = Decimal("0.01") + + self.assertGreater(self.market.get_balance("BTC"), 0.00005) + trading_pair = "DASH-BTC" + + price: Decimal = self.market.get_price(trading_pair, True) * Decimal(1.1) + quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) + + order_id, _ = self.place_order( + True, trading_pair, quantized_amount, OrderType.LIMIT, price, + [(3, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_COMPLETED)] + ) + [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) + order_completed_event: BuyOrderCompletedEvent = order_completed_event + trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log + if isinstance(t, OrderFilledEvent)] + base_amount_traded: Decimal = sum(t.amount for t in trade_events) + quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) + + self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) + self.assertEqual(order_id, order_completed_event.order_id) + self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) + self.assertEqual("DASH", order_completed_event.base_asset) + self.assertEqual("BTC", order_completed_event.quote_asset) + self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) + self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) + self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id + for event in self.market_logger.event_log])) + # Reset the logs + self.market_logger.clear() + + def test_limit_sell(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_LIMIT_SELL_ORDER) + + trading_pair = "DASH-BTC" + self.assertGreater(self.market.get_balance("DASH"), 0.01) + + price: Decimal = self.market.get_price(trading_pair, False) * Decimal(0.9) + amount: Decimal = Decimal("0.01") + quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) + + order_id, _ = self.place_order( + False, trading_pair, quantized_amount, OrderType.LIMIT, price, + [(3, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_COMPLETED)] + ) + [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) + order_completed_event: SellOrderCompletedEvent = order_completed_event + trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] + base_amount_traded = sum(t.amount for t in trade_events) + quote_amount_traded = sum(t.amount * t.price for t in trade_events) + + self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) + self.assertEqual(order_id, order_completed_event.order_id) + self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) + self.assertEqual("DASH", order_completed_event.base_asset) + self.assertEqual("BTC", order_completed_event.quote_asset) + self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) + self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) + self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id + for event in self.market_logger.event_log])) + # Reset the logs + self.market_logger.clear() + + def test_limit_maker_rejections(self): + if API_MOCK_ENABLED: + return + trading_pair = "DASH-BTC" + + # Try to put a buy limit maker order that is going to match, this should triggers order failure event. + price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') + price: Decimal = self.market.quantize_order_price(trading_pair, price) + amount = self.market.quantize_order_amount(trading_pair, 1) + + order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) + [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) + self.assertEqual(order_id, order_failure_event.order_id) + + self.market_logger.clear() + + # Try to put a sell limit maker order that is going to match, this should triggers order failure event. + price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') + price: Decimal = self.market.quantize_order_price(trading_pair, price) + amount = self.market.quantize_order_amount(trading_pair, 1) + + order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) + [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) + self.assertEqual(order_id, order_failure_event.order_id) + + def test_limit_makers_unfilled(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_UNFILLED_ORDER1) + + self.assertGreater(self.market.get_balance("BTC"), 0.00005) + trading_pair = "DASH-BTC" + + current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.8') + quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) + bid_amount: Decimal = Decimal('0.01') + quantized_bid_amount: Decimal = self.market.quantize_order_amount(trading_pair, bid_amount) + + current_ask_price: Decimal = self.market.get_price(trading_pair, False) + quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, current_ask_price) + ask_amount: Decimal = Decimal('0.01') + quantized_ask_amount: Decimal = self.market.quantize_order_amount(trading_pair, ask_amount) + + order_id1, exch_order_id_1 = self.place_order( + True, trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price, + [(3, FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CREATED)] + ) + [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) + order_created_event: BuyOrderCreatedEvent = order_created_event + self.assertEqual(order_id1, order_created_event.order_id) + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_UNFILLED_ORDER2) + + order_id2, exch_order_id_2 = self.place_order( + False, trading_pair, quantized_ask_amount, OrderType.LIMIT, quantize_ask_price, + [(3, FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CREATED)] + ) + [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) + order_created_event: BuyOrderCreatedEvent = order_created_event + self.assertEqual(order_id2, order_created_event.order_id) + + if API_MOCK_ENABLED: + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, + FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CANCELED, delay=3) + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, + FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CANCELED, delay=3) + + self.web_app.update_response("delete", PRIVET_API_BASE_URL, "/api/v1/orders", "") + + self.run_parallel(asyncio.sleep(1)) + [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) + for cr in cancellation_results: + self.assertEqual(cr.success, True) + + def test_market_buy(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_MARKET_BUY_ORDER) + + amount: Decimal = Decimal("0.01") + + self.assertGreater(self.market.get_balance("BTC"), 0.00005) + trading_pair = "DASH-BTC" + + price: Decimal = self.market.get_price(trading_pair, True) + quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) + + order_id, _ = self.place_order( + True, trading_pair, quantized_amount, OrderType.MARKET, price, + [(3, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_COMPLETED)] + ) + [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) + order_completed_event: BuyOrderCompletedEvent = order_completed_event + trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log + if isinstance(t, OrderFilledEvent)] + base_amount_traded: Decimal = sum(t.amount for t in trade_events) + quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) + + self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) + self.assertEqual(order_id, order_completed_event.order_id) + self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) + self.assertEqual("DASH", order_completed_event.base_asset) + self.assertEqual("BTC", order_completed_event.quote_asset) + self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) + self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) + self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id + for event in self.market_logger.event_log])) + # Reset the logs + self.market_logger.clear() + + def test_market_sell(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_MARKET_SELL_ORDER) + + trading_pair = "DASH-BTC" + self.assertGreater(self.market.get_balance("DASH"), 0.01) + + price: Decimal = self.market.get_price(trading_pair, False) + amount: Decimal = Decimal("0.01") + quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) + + order_id, _ = self.place_order( + False, trading_pair, quantized_amount, OrderType.MARKET, price, + [(3, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_COMPLETED)] + ) + [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) + order_completed_event: SellOrderCompletedEvent = order_completed_event + trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] + base_amount_traded = sum(t.amount for t in trade_events) + quote_amount_traded = sum(t.amount * t.price for t in trade_events) + + self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) + self.assertEqual(order_id, order_completed_event.order_id) + self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) + self.assertEqual("DASH", order_completed_event.base_asset) + self.assertEqual("BTC", order_completed_event.quote_asset) + self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) + self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) + self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id + for event in self.market_logger.event_log])) + # Reset the logs + self.market_logger.clear() + + def test_cancel_order(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_CANCEL_BUY_ORDER) + + self.web_app.update_response("delete", PRIVET_API_BASE_URL, "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '') + + amount: Decimal = Decimal("0.01") + + self.assertGreater(self.market.get_balance("BTC"), 0.00005) + trading_pair = "DASH-BTC" + + # make worst price so order wont be executed + price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.5') + quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) + + order_id, exch_order_id = self.place_order( + True, trading_pair, quantized_amount, OrderType.LIMIT, price, + [(3, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_COMPLETED)] + ) + [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) + + if API_MOCK_ENABLED: + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, + FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_CANCELED, delay=3) + + self.cancel_order(trading_pair, order_id, exch_order_id) + [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) + order_cancelled_event: OrderCancelledEvent = order_cancelled_event + self.assertEqual(order_cancelled_event.order_id, order_id) + + def test_cancel_all(self): + + if API_MOCK_ENABLED: + self.web_app.update_response("delete", PRIVET_API_BASE_URL, "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '') + + self.assertGreater(self.market.get_balance("BTC"), 0.00005) + self.assertGreater(self.market.get_balance("DASH"), 0.01) + trading_pair = "DASH-BTC" + + # make worst price so order wont be executed + current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.5') + quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) + bid_amount: Decimal = Decimal('0.01') + quantized_bid_amount: Decimal = self.market.quantize_order_amount(trading_pair, bid_amount) + + # make worst price so order wont be executed + current_ask_price: Decimal = self.market.get_price(trading_pair, False) * Decimal('2') + quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, current_ask_price) + ask_amount: Decimal = Decimal('0.01') + quantized_ask_amount: Decimal = self.market.quantize_order_amount(trading_pair, ask_amount) + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_CANCEL_ALL_ORDER1) + + _, exch_order_id_1 = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, + quantize_bid_price) + + if API_MOCK_ENABLED: + self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", + FixtureBeaxy.TEST_CANCEL_ALL_ORDER2) + + _, exch_order_id_2 = self.place_order(False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER, + quantize_ask_price) + self.run_parallel(asyncio.sleep(1)) + + [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) + + if API_MOCK_ENABLED: + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, + FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER1_CANCELED, delay=3) + HummingWsServerFactory.send_str_threadsafe(BeaxyConstants.TradingApi.WS_BASE_URL, + FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER2_CANCELED, delay=3) + + for cr in cancellation_results: + self.assertEqual(cr.success, True) + + def test_cancel_empty(self): + trading_pair = "DASH-BTC" + self.cancel_order(trading_pair, '123', '123') diff --git a/test/integration/test_beaxy_order_book_tracker.py b/test/integration/test_beaxy_order_book_tracker.py new file mode 100644 index 0000000000..42896d8a94 --- /dev/null +++ b/test/integration/test_beaxy_order_book_tracker.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +from os.path import ( + join, + realpath +) +import sys; sys.path.insert(0, realpath(join(__file__, "../../../"))) +from hummingbot.core.event.event_logger import EventLogger +from hummingbot.core.event.events import ( + OrderBookEvent +) +import asyncio +import logging +import unittest +from typing import Dict, Optional, List + +from hummingbot.connector.exchange.beaxy.beaxy_order_book_tracker import BeaxyOrderBookTracker +from hummingbot.core.data_type.order_book import OrderBook +from hummingbot.core.data_type.order_book_tracker import OrderBookTrackerDataSource +from hummingbot.core.utils.async_utils import safe_ensure_future, safe_gather + + +class BeaxyOrderBookTrackerUnitTest(unittest.TestCase): + order_book_tracker: Optional[BeaxyOrderBookTracker] = None + events: List[OrderBookEvent] = [ + OrderBookEvent.TradeEvent + ] + trading_pairs: List[str] = [ + "BXY-BTC", + ] + + integrity_test_max_volume = 5 # Max volume in asks and bids for the book to be ready for tests + daily_volume = 2500 # Approximate total daily volume in BTC for this exchange for sanity test + book_enties = 5 # Number of asks and bids (each) for the book to be ready for tests + + @classmethod + def setUpClass(cls): + cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() + cls.order_book_tracker: BeaxyOrderBookTracker = BeaxyOrderBookTracker(cls.trading_pairs) + cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(cls.order_book_tracker.start()) + cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) + + @classmethod + async def wait_til_tracker_ready(cls): + print("Waiting for order book to fill...") + while True: + book_present = cls.trading_pairs[0] in cls.order_book_tracker.order_books + enough_asks = False + enough_bids = False + enough_ask_rows = False + enough_bid_rows = False + if book_present: + ask_volume = sum(i.amount for i in cls.order_book_tracker.order_books[cls.trading_pairs[0]].ask_entries()) + ask_count = sum(1 for i in cls.order_book_tracker.order_books[cls.trading_pairs[0]].ask_entries()) + + bid_volume = sum(i.amount for i in cls.order_book_tracker.order_books[cls.trading_pairs[0]].bid_entries()) + bid_count = sum(1 for i in cls.order_book_tracker.order_books[cls.trading_pairs[0]].bid_entries()) + + enough_asks = ask_volume >= cls.integrity_test_max_volume + enough_bids = bid_volume >= cls.integrity_test_max_volume + + enough_ask_rows = ask_count >= cls.book_enties + enough_bid_rows = bid_count >= cls.book_enties + + print("Bid volume in book: %f (in %d bids), ask volume in book: %f (in %d asks)" % (bid_volume, bid_count, ask_volume, ask_count)) + + if book_present and enough_asks and enough_bids and enough_ask_rows and enough_bid_rows: + print("Initialized real-time order books.") + return + await asyncio.sleep(1) + + async def run_parallel_async(self, *tasks, timeout=None): + future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) + timer = 0 + while not future.done(): + if timeout and timer > timeout: + raise Exception("Time out running parallel async task in tests.") + timer += 1 + await asyncio.sleep(1.0) + return future.result() + + def run_parallel(self, *tasks): + return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) + + def setUp(self): + self.event_logger = EventLogger() + for event_tag in self.events: + for trading_pair, order_book in self.order_book_tracker.order_books.items(): + order_book.add_listener(event_tag, self.event_logger) + + def test_tracker_integrity(self): + order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books + sut_book: OrderBook = order_books[self.trading_pairs[0]] + + self.assertGreater(sut_book.get_price(True), sut_book.get_price(False)) + + self.assertGreaterEqual(sut_book.get_price_for_volume(True, self.integrity_test_max_volume).result_price, + sut_book.get_price(True)) + + self.assertLessEqual(sut_book.get_price_for_volume(False, self.integrity_test_max_volume).result_price, + sut_book.get_price(False)) + + previous_price = sys.float_info.max + for bid_row in sut_book.bid_entries(): + self.assertTrue(previous_price >= bid_row.price) + previous_price = bid_row.price + + previous_price = 0 + for ask_row in sut_book.ask_entries(): + self.assertTrue(previous_price <= ask_row.price) + previous_price = ask_row.price + + def test_order_book_data_source(self): + self.assertTrue(isinstance(self.order_book_tracker.data_source, OrderBookTrackerDataSource)) + + def test_get_active_exchange_markets(self): + [active_markets_df] = self.run_parallel(self.order_book_tracker.data_source.get_active_exchange_markets()) + self.assertGreater(active_markets_df.size, 0) + self.assertTrue("baseAsset" in active_markets_df) + self.assertTrue("quoteAsset" in active_markets_df) + self.assertTrue("USDVolume" in active_markets_df) + + def test_get_trading_pairs(self): + [trading_pairs] = self.run_parallel(self.order_book_tracker.data_source.get_trading_pairs()) + self.assertGreater(len(trading_pairs), 0) + + +def main(): + logging.basicConfig(level=logging.INFO) + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/test/integration/test_beaxy_user_stream_tracker.py b/test/integration/test_beaxy_user_stream_tracker.py new file mode 100644 index 0000000000..f27cbf6fea --- /dev/null +++ b/test/integration/test_beaxy_user_stream_tracker.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +from os.path import join, realpath +import sys + +import conf +from hummingbot.connector.exchange.beaxy.beaxy_auth import BeaxyAuth + +from hummingbot.connector.exchange.beaxy.beaxy_user_stream_tracker import BeaxyUserStreamTracker + +import asyncio +import logging +import unittest + +sys.path.insert(0, realpath(join(__file__, "../../../"))) + +logging.basicConfig(level=logging.DEBUG) + + +class BeaxyUserStreamTrackerUnitTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + beaxy_auth = BeaxyAuth(conf.beaxy_api_key, + conf.beaxy_api_secret) + cls.user_stream_tracker: BeaxyUserStreamTracker = BeaxyUserStreamTracker(beaxy_auth) + cls.user_stream_tracker_task = asyncio.ensure_future(cls.user_stream_tracker.start()) + + def test_user_stream(self): + # Wait process some msgs. + self.ev_loop.run_until_complete(asyncio.sleep(120.0)) + print(self.user_stream_tracker.user_stream) + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main()