def test_fetch_trading_pairs(self, mock_api, mock_utils): # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange() mock_utils.return_value = self.trading_pair url = utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { # Truncated Response "symbols": [ { "symbol": self.ex_trading_pair, "status": "TRADING", "baseAsset": self.base_asset, "quoteAsset": self.quote_asset, }, ] } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result: Dict[str] = self.ev_loop.run_until_complete( self.data_source.fetch_trading_pairs()) self.assertEqual(1, len(result)) self.assertTrue(self.trading_pair in result)
async def get_all_mid_prices(domain="com") -> Dict[str, Decimal]: """ Returns the mid price of all trading pairs, obtaining the information from the exchange. This functionality is required by the market price strategy. :param domain: Domain to use for the connection with the exchange (either "com" or "us"). Default value is "com" :return: Dictionary with the trading pair as key, and the mid price as value """ local_api_factory = build_api_factory() rest_assistant = await local_api_factory.get_rest_assistant() throttler = BinanceAPIOrderBookDataSource._get_throttler_instance() url = binance_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=domain) request = RESTRequest(method=RESTMethod.GET, url=url) async with throttler.execute_task( limit_id=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL): resp: RESTResponse = await rest_assistant.call(request=request) resp_json = await resp.json() ret_val = {} for record in resp_json: try: pair = await BinanceAPIOrderBookDataSource.trading_pair_associated_to_exchange_symbol( symbol=record["symbol"], domain=domain) ret_val[pair] = ((Decimal(record.get("bidPrice", "0")) + Decimal(record.get("askPrice", "0"))) / Decimal("2")) except KeyError: # Ignore results for pairs that are not tracked continue return ret_val
async def _init_trading_pair_symbols( cls, domain: str = "com", api_factory: Optional[WebAssistantsFactory] = None, throttler: Optional[AsyncThrottler] = None): """ Initialize mapping of trade symbols in exchange notation to trade symbols in client notation """ mapping = bidict() local_api_factory = api_factory or build_api_factory() rest_assistant = await local_api_factory.get_rest_assistant() local_throttler = throttler or cls._get_throttler_instance() url = binance_utils.public_rest_url( path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=domain) request = RESTRequest(method=RESTMethod.GET, url=url) try: async with local_throttler.execute_task( limit_id=CONSTANTS.EXCHANGE_INFO_PATH_URL): response: RESTResponse = await rest_assistant.call( request=request) if response.status == 200: data = await response.json() for symbol_data in filter( binance_utils.is_exchange_information_valid, data["symbols"]): mapping[symbol_data[ "symbol"]] = f"{symbol_data['baseAsset']}-{symbol_data['quoteAsset']}" except Exception as ex: cls.logger().error( f"There was an error requesting exchange info ({str(ex)})") cls._trading_pair_symbol_map[domain] = mapping
async def get_snapshot(self, trading_pair: str, limit: int = 1000) -> Dict[str, Any]: """ Retrieves a copy of the full order book from the exchange, for a particular trading pair. :param trading_pair: the trading pair for which the order book will be retrieved :param limit: the depth of the order book to retrieve :return: the response from the exchange (JSON dictionary) """ rest_assistant = await self._get_rest_assistant() params = { "symbol": await self.exchange_symbol_associated_to_pair( trading_pair=trading_pair, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler) } if limit != 0: params["limit"] = str(limit) url = binance_utils.public_rest_url( path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self._domain) request = RESTRequest(method=RESTMethod.GET, url=url, params=params) async with self._throttler.execute_task( limit_id=CONSTANTS.SNAPSHOT_PATH_URL): response: RESTResponse = await rest_assistant.call(request=request) if response.status != 200: raise IOError( f"Error fetching market snapshot for {trading_pair}. " f"Response: {response}.") data = await response.json() return data
def test_get_all_mid_prices(self, mock_api): url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) mock_response: List[Dict[str, Any]] = [ { # Truncated Response "symbol": self.ex_trading_pair, "bidPrice": "99", "askPrice": "101", }, { # Truncated Response for unrecognized pair "symbol": "BCCBTC", "bidPrice": "99", "askPrice": "101", } ] mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_all_mid_prices()) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair])
async def get_snapshot( trading_pair: str, limit: int = 1000, domain: str = "com", throttler: Optional[AsyncThrottler] = None) -> Dict[str, Any]: throttler = throttler or BinanceAPIOrderBookDataSource._get_throttler_instance( ) params: Dict = {"limit": str(limit), "symbol": binance_utils.convert_to_exchange_trading_pair(trading_pair)} if limit != 0 \ else {"symbol": binance_utils.convert_to_exchange_trading_pair(trading_pair)} async with aiohttp.ClientSession() as client: async with throttler.execute_task( limit_id=CONSTANTS.SNAPSHOT_PATH_URL): url = binance_utils.public_rest_url( path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=domain) async with client.get(url, params=params) as response: response: aiohttp.ClientResponse = response if response.status != 200: raise IOError( f"Error fetching market snapshot for {trading_pair}. " f"Response: {response}.") data: Dict[str, Any] = await response.json() # Need to add the symbol into the snapshot message for the Kafka message queue. # Because otherwise, there'd be no way for the receiver to know which market the # snapshot belongs to. return data
def test_get_snapshot_catch_exception(self, mock_api): url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): self.ev_loop.run_until_complete( self.data_source.get_snapshot(self.trading_pair))
def test_get_snapshot_successful(self, mock_api): url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=ujson.dumps(self._snapshot_response())) result: Dict[str, Any] = self.ev_loop.run_until_complete( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(self._snapshot_response(), result)
def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.ev_loop.run_until_complete( self.data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue()))
def test_fetch_trading_pairs_exception_raised(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = {} url = utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result))
def test_fetch_trading_pairs_exception_raised(self, mock_api, mock_utils): # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange() mock_utils.return_value = self.trading_pair url = utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.ev_loop.run_until_complete( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result))
def test_get_new_order_book(self, mock_api): url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { "lastUpdateId": 1, "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]] } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result: OrderBook = self.ev_loop.run_until_complete( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1, result.snapshot_uid)
async def get_last_traded_price( cls, trading_pair: str, domain: str = "com", throttler: Optional[AsyncThrottler] = None) -> float: throttler = throttler or cls._get_throttler_instance() async with aiohttp.ClientSession() as client: async with throttler.execute_task( limit_id=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL): url = binance_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=domain) resp = await client.get( f"{url}?symbol={binance_utils.convert_to_exchange_trading_pair(trading_pair)}" ) resp_json = await resp.json() return float(resp_json["lastPrice"])
async def _get_last_traded_price(cls, trading_pair: str, domain: str, rest_assistant: RESTAssistant, throttler: AsyncThrottler) -> float: url = binance_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=domain) symbol = await cls.exchange_symbol_associated_to_pair( trading_pair=trading_pair, domain=domain, throttler=throttler) request = RESTRequest(method=RESTMethod.GET, url=f"{url}?symbol={symbol}") async with throttler.execute_task( limit_id=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL): resp: RESTResponse = await rest_assistant.call(request=request) if resp.status == 200: resp_json = await resp.json() return float(resp_json["lastPrice"])
async def _api_request(self, method: RESTMethod, path_url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, is_auth_required: bool = False) -> Dict[str, Any]: headers = { "Content-Type": "application/json" if method == RESTMethod.POST else "application/x-www-form-urlencoded" } client = await self._get_rest_assistant() if is_auth_required: url = binance_utils.private_rest_url(path_url, domain=self._domain) else: url = binance_utils.public_rest_url(path_url, domain=self._domain) request = RESTRequest(method=method, url=url, data=data, params=params, headers=headers, is_auth_required=is_auth_required) async with self._throttler.execute_task(limit_id=path_url): response = await client.call(request) if response.status != 200: data = await response.text() raise IOError( f"Error fetching data from {url}. HTTP status is {response.status} ({data})." ) try: parsed_response = await response.json() except Exception: raise IOError(f"Error parsing data from {response}.") if "code" in parsed_response and "msg" in parsed_response: raise IOError( f"The request to Binance failed. Error: {parsed_response}. Request: {request}" ) return parsed_response
async def get_all_mid_prices(domain="com") -> Optional[Decimal]: throttler = BinanceAPIOrderBookDataSource._get_throttler_instance() async with aiohttp.ClientSession() as client: async with throttler.execute_task( limit_id=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL): url = binance_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=domain) resp = await client.get(url) resp_json = await resp.json() ret_val = {} for record in resp_json: pair = binance_utils.convert_from_exchange_trading_pair( record["symbol"]) ret_val[pair] = ( Decimal(record.get("bidPrice", "0")) + Decimal(record.get("askPrice", "0"))) / Decimal("2") return ret_val
def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(12345, msg.update_id)
def test_get_last_trade_prices(self, mock_api): url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { # Truncated Response "lastPrice": "100", } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result: Dict[str, float] = self.ev_loop.run_until_complete( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair])
def test_get_last_trade_prices(self, mock_api): url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) url = f"{url}?symbol={self.base_asset}{self.quote_asset}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "symbol": "BNBBTC", "priceChange": "-94.99999800", "priceChangePercent": "-95.960", "weightedAvgPrice": "0.29628482", "prevClosePrice": "0.10002000", "lastPrice": "100.0", "lastQty": "200.00000000", "bidPrice": "4.00000000", "bidQty": "100.00000000", "askPrice": "4.00000200", "askQty": "100.00000000", "openPrice": "99.00000000", "highPrice": "100.00000000", "lowPrice": "0.10000000", "volume": "8913.30000000", "quoteVolume": "15.30000000", "openTime": 1499783499040, "closeTime": 1499869899040, "firstId": 28385, "lastId": 28460, "count": 76, } mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair])
def test_listen_for_order_book_snapshots_log_exception(self, mock_api): msg_queue: asyncio.Queue = asyncio.Queue() url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) with self.assertRaises(asyncio.TimeoutError): self.ev_loop.run_until_complete( asyncio.wait_for(self.listening_task, 1)) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." ))
def test_get_last_price(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = { "com": bidict({f"{self.binance_ex_trading_pair}": self.trading_pair}) } url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { # truncated response "symbol": self.binance_ex_trading_pair, "lastPrice": "1", } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( market_price.get_last_price(exchange="binance", trading_pair=self.trading_pair)) self.assertEqual(result, Decimal("1.0"))
def test_listen_for_order_book_snapshots_log_exception( self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) url = utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." ))
def test_get_all_mid_prices(self, mock_api, mock_utils): # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange() mock_utils.return_value = self.trading_pair url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: List[Dict[str, Any]] = [{ # Truncated Response "symbol": self.ex_trading_pair, "bidPrice": "99", "askPrice": "101", }] mock_api.get(regex_url, body=ujson.dumps(mock_response)) result: Dict[str, float] = self.ev_loop.run_until_complete( self.data_source.get_all_mid_prices()) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair])
async def fetch_trading_pairs(domain="com") -> List[str]: try: throttler = BinanceAPIOrderBookDataSource._get_throttler_instance() async with aiohttp.ClientSession() as client: async with throttler.execute_task( limit_id=CONSTANTS.EXCHANGE_INFO_PATH_URL): url = binance_utils.public_rest_url( path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=domain) async with client.get(url, timeout=10) as response: if response.status == 200: data = await response.json() # fetch d["symbol"] for binance us/com raw_trading_pairs = [ d["symbol"] for d in data["symbols"] if d["status"] == "TRADING" ] trading_pair_targets = [ f"{d['baseAsset']}-{d['quoteAsset']}" for d in data["symbols"] if d["status"] == "TRADING" ] trading_pair_list: List[str] = [] for raw_trading_pair, pair_target in zip( raw_trading_pairs, trading_pair_targets): trading_pair: Optional[ str] = binance_utils.convert_from_exchange_trading_pair( raw_trading_pair) if trading_pair is not None and trading_pair == pair_target: trading_pair_list.append(trading_pair) return trading_pair_list except Exception: # Do nothing if the request fails -- there will be no autocomplete for binance trading pairs pass return []
def test_get_binance_mid_price(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = { "com": bidict({f"{self.base_asset}{self.quote_asset}": self.trading_pair}) } url = utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: List[Dict[str, Any]] = [ { # Truncated Response "symbol": self.binance_ex_trading_pair, "bidPrice": "1.0", "askPrice": "2.0", }, ] mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( market_price.get_binance_mid_price(trading_pair=self.trading_pair)) self.assertEqual(result, Decimal("1.5"))
def test_public_rest_url(self): path_url = "/TEST_PATH" domain = "com" expected_url = CONSTANTS.REST_URL.format( domain) + CONSTANTS.PUBLIC_API_VERSION + path_url self.assertEqual(expected_url, utils.public_rest_url(path_url, domain))
def test_fetch_trading_pairs(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = {} url = utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) mock_response: Dict[str, Any] = { "timezone": "UTC", "serverTime": 1639598493658, "rateLimits": [], "exchangeFilters": [], "symbols": [ { "symbol": "ETHBTC", "status": "TRADING", "baseAsset": "ETH", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["SPOT", "MARGIN"] }, { "symbol": "LTCBTC", "status": "TRADING", "baseAsset": "LTC", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["SPOT", "MARGIN"] }, { "symbol": "BNBBTC", "status": "TRADING", "baseAsset": "BNB", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["MARGIN"] }, ] } mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(2, len(result)) self.assertIn("ETH-BTC", result) self.assertIn("LTC-BTC", result) self.assertNotIn("BNB-BTC", result)