def test_new_account_position_detected_on_stream_event( self, mock_api, ws_connect_mock): url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) listen_key_response = {"listenKey": "someListenKey"} mock_api.post(regex_url, body=json.dumps(listen_key_response)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.ev_loop.create_task(self.exchange._user_stream_tracker.start()) self.assertEqual(len(self.exchange.account_positions), 0) account_update = self._get_account_update_ws_event_single_position_dict( ) self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps(account_update)) url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() mock_api.get(regex_url, body=json.dumps(positions)) self.ev_loop.create_task(self.exchange._user_stream_event_listener()) asyncio.get_event_loop().run_until_complete(asyncio.sleep(1)) self.assertEqual(len(self.exchange.account_positions), 1)
def test_closed_account_position_removed_on_stream_event(self, mock_api, ws_connect_mock): url = utils.rest_url( CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2 ) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() mock_api.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.async_run_with_timeout(task) url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) listen_key_response = {"listenKey": self.listen_key} mock_api.post(regex_url, body=json.dumps(listen_key_response)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.ev_loop.create_task(self.exchange._user_stream_tracker.start()) self.assertEqual(len(self.exchange.account_positions), 1) account_update = self._get_account_update_ws_event_single_position_dict() account_update["a"]["P"][0]["pa"] = 0 self.mocking_assistant.add_websocket_aiohttp_message(ws_connect_mock.return_value, json.dumps(account_update)) self.ev_loop.create_task(self.exchange._user_stream_event_listener()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertEqual(len(self.exchange.account_positions), 0)
def test_rest_url_main_domain(self): path_url = "/TEST_PATH_URL" expected_url = path_url = "/TEST_PATH_URL" expected_url = f"{CONSTANTS.PERPETUAL_BASE_URL}{CONSTANTS.API_VERSION_V2}{path_url}" self.assertEqual(expected_url, utils.rest_url(path_url, api_version=CONSTANTS.API_VERSION_V2)) self.assertEqual(expected_url, utils.rest_url(path_url, api_version=CONSTANTS.API_VERSION_V2))
def test_listen_for_user_stream_iter_message_throws_exception( self, mock_api, _, mock_ws): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"listenKey": self.listen_key} mock_api.post(regex_url, body=ujson.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = Exception("TEST ERROR") mock_ws.return_value.closed = False mock_ws.return_value.close.side_effect = Exception self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except Exception: pass self.assertTrue( self._is_logged( "INFO", f"Successfully obtained listen key {self.listen_key}")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR", ))
def test_listen_for_user_stream_create_websocket_connection_failed( self, mock_api, mock_ws): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.side_effect = Exception("TEST ERROR.") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except asyncio.exceptions.TimeoutError: pass self.assertTrue( self._is_logged( "INFO", f"Successfully obtained listen key {self.listen_key}")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.", ))
async def fetch_trading_pairs( domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None) -> List[str]: url = utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL, domain=domain) throttler = throttler or BinancePerpetualAPIOrderBookDataSource._get_throttler_instance( ) async with throttler.execute_task( limit_id=CONSTANTS.EXCHANGE_INFO_URL): async with aiohttp.ClientSession() as client: async with client.get(url=url, timeout=10) as response: if response.status == 200: data = await response.json() # fetch d["pair"] for binance perpetual raw_trading_pairs = [ d["pair"] 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 = 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 return []
def test_get_funding_info(self, mock_api): self.assertNotIn(self.trading_pair, self.data_source._funding_info) url = utils.rest_url(CONSTANTS.MARK_PRICE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "symbol": self.ex_trading_pair, "markPrice": "46382.32704603", "indexPrice": "46385.80064948", "estimatedSettlePrice": "46510.13598963", "lastFundingRate": "0.00010000", "interestRate": "0.00010000", "nextFundingTime": 1641312000000, "time": 1641288825000, } mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_funding_info(trading_pair=self.trading_pair)) self.assertIsInstance(result, FundingInfo) self.assertEqual(result.trading_pair, self.trading_pair) self.assertEqual(result.index_price, Decimal(mock_response["indexPrice"])) self.assertEqual(result.mark_price, Decimal(mock_response["markPrice"])) self.assertEqual(result.next_funding_utc_timestamp, mock_response["nextFundingTime"]) self.assertEqual(result.rate, Decimal(mock_response["lastFundingRate"]))
async def get_snapshot( trading_pair: str, limit: int = 1000, domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None, api_factory: WebAssistantsFactory = None ) -> Dict[str, Any]: ob_source_cls = BinancePerpetualAPIOrderBookDataSource try: api_factory = api_factory or utils.build_api_factory() rest_assistant = await api_factory.get_rest_assistant() params = {"symbol": await ob_source_cls.convert_to_exchange_trading_pair( hb_trading_pair=trading_pair, domain=domain, throttler=throttler)} if limit != 0: params.update({"limit": str(limit)}) url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain) throttler = throttler or ob_source_cls._get_throttler_instance() async with throttler.execute_task(limit_id=CONSTANTS.SNAPSHOT_REST_URL): request = RESTRequest( method=RESTMethod.GET, url=url, params=params, ) response = await rest_assistant.call(request=request) if response.status != 200: raise IOError(f"Error fetching Binance market snapshot for {trading_pair}.") data: Dict[str, Any] = await response.json() return data except asyncio.CancelledError: raise except Exception: raise
def test_listen_for_user_stream_handle_ping_frame(self, mock_api, mock_ws): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, "", aiohttp.WSMsgType.PING) self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, self._simulate_user_update_event()) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) msg = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(msg, self._simulate_user_update_event) self.assertTrue( self._is_logged("DEBUG", "Received PING frame. Sending PONG frame..."))
async def get_snapshot( trading_pair: str, limit: int = 1000, domain: str = CONSTANTS.DOMAIN, throttler: Optional[AsyncThrottler] = None) -> Dict[str, Any]: try: params = {"symbol": convert_to_exchange_trading_pair(trading_pair)} if limit != 0: params.update({"limit": str(limit)}) url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain) throttler = throttler or BinancePerpetualAPIOrderBookDataSource._get_throttler_instance( ) async with throttler.execute_task( limit_id=CONSTANTS.SNAPSHOT_REST_URL): async with aiohttp.ClientSession() as client: async with client.get(url=url, params=params) as response: if response.status != 200: raise IOError( f"Error fetching Binance market snapshot for {trading_pair}." ) data: Dict[str, Any] = await response.json() return data except asyncio.CancelledError: raise except Exception: raise
def test_listen_for_order_book_snapshots_logs_exception_error_with_response( self, mock_api): url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "m": 1, "i": 2, } mock_api.get(regex_url, body=json.dumps(mock_response), callback=self.resume_test_callback) msg_queue: asyncio.Queue = asyncio.Queue() 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", "Unexpected error occurred fetching orderbook snapshots. Retrying in 5 seconds..." ))
def test_manage_listen_key_task_loop_keep_alive_failed(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, status=400, body=ujson.dumps(self._error_response()), callback=self._mock_responses_done_callback) self.data_source._current_listen_key = self.listen_key # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached self.data_source._last_listen_key_ping_ts = 0 self.listening_task = self.ev_loop.create_task( self.data_source._manage_listen_key_task_loop()) self.async_run_with_timeout(self.mock_done_event.wait()) self.assertTrue( self._is_logged("ERROR", "Error occurred renewing listen key... ")) self.assertIsNone(self.data_source._current_listen_key) self.assertFalse( self.data_source._listen_key_initialized_event.is_set())
def test_listen_for_order_book_snapshots_successful(self, mock_api): url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "lastUpdateId": 1027024, "E": 1589436922972, "T": 1589436922959, "bids": [["10", "1"]], "asks": [["11", "1"]], } mock_api.get(regex_url, body=json.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) result = self.async_run_with_timeout(msg_queue.get()) self.assertIsInstance(result, OrderBookMessage) self.assertEqual(OrderBookMessageType.SNAPSHOT, result.type) self.assertTrue(result.has_update_id) self.assertEqual(result.update_id, 1027024) self.assertEqual(self.trading_pair, result.content["trading_pair"])
def test_rest_url_testnet_domain(self): path_url = "/TEST_PATH_URL" expected_url = f"{CONSTANTS.TESTNET_BASE_URL}{CONSTANTS.API_VERSION_V2}{path_url}" self.assertEqual( expected_url, utils.rest_url(path_url=path_url, domain="testnet", api_version=CONSTANTS.API_VERSION_V2) )
def test_init_trading_pair_symbols_successful(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { # Truncated Responses "symbols": [ { "symbol": self.ex_trading_pair, "pair": self.ex_trading_pair, "baseAsset": self.base_asset, "quoteAsset": self.quote_asset, "status": "TRADING", }, { "symbol": "INACTIVEMARKET", "status": "INACTIVE" }, ], } mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) self.async_run_with_timeout( self.data_source.init_trading_pair_symbols(domain=self.domain)) self.assertEqual(1, len(self.data_source._trading_pair_symbol_map))
async def get_last_traded_price(cls, trading_pair: str, domain: str = CONSTANTS.DOMAIN) -> float: url = utils.rest_url(path_url=CONSTANTS.TICKER_PRICE_CHANGE_URL, domain=domain) params = {"symbol": convert_to_exchange_trading_pair(trading_pair)} async with aiohttp.ClientSession() as client: async with client.get(url=url, params=params) as resp: resp_json = await resp.json() return float(resp_json["lastPrice"])
def test_fetch_trading_pairs_failure(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=ujson.dumps({"ERROR"})) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs(domain=self.domain)) self.assertEqual(0, len(result))
def test_set_position_initial_mode_unchanged(self, mock_api): self.exchange._position_mode = PositionMode.ONEWAY url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = {"dualSidePosition": False} # True: Hedge Mode; False: One-way Mode mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) task = self.ev_loop.create_task(self.exchange._set_position_mode(PositionMode.ONEWAY)) self.async_run_with_timeout(task) self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode)
def test_ping_listen_key_successful(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, body=ujson.dumps({})) self.data_source._current_listen_key = self.listen_key result: bool = self.async_run_with_timeout( self.data_source.ping_listen_key()) self.assertTrue(result)
def test_get_listen_key_exception_raised(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, status=400, body=ujson.dumps(self._error_response)) with self.assertRaises(IOError): self.async_run_with_timeout(self.data_source.get_listen_key())
def test_init_trading_pair_symbols_failure(self, mock_api): BinancePerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {} url = utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"])) map = self.async_run_with_timeout( self.data_source.trading_pair_symbol_map(domain=self.domain)) self.assertEqual(0, len(map))
def test_get_listen_key_successful(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) result: str = self.async_run_with_timeout( self.data_source.get_listen_key()) self.assertEqual(self.listen_key, result)
def test_set_position_mode_initial_mode_is_none(self, mock_api): self.assertIsNone(self.exchange.position_mode) url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = {"dualSidePosition": False} # True: Hedge Mode; False: One-way Mode post_position_mode_response = {"code": 200, "msg": "success"} mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) mock_api.post(regex_url, body=json.dumps(post_position_mode_response)) task = self.ev_loop.create_task(self.exchange._set_position_mode(PositionMode.HEDGE)) self.async_run_with_timeout(task) self.assertEqual(PositionMode.HEDGE, self.exchange.position_mode)
async def get_listen_key(self): async with aiohttp.ClientSession() as client: async with self._throttler.execute_task( limit_id=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT): response: aiohttp.ClientResponse = await client.post( url=utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, self._domain), headers={"X-MBX-APIKEY": self._api_key}) if response.status != 200: raise IOError( f"Error fetching Binance Perpetual user stream listen key. " f"HTTP status is {response.status}.") data: Dict[str, str] = await response.json() return data["listenKey"]
def test_get_snapshot_exception_raised(self, mock_api): url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"])) with self.assertRaises(IOError) as context: self.async_run_with_timeout( self.data_source.get_snapshot(trading_pair=self.trading_pair, domain=self.domain)) self.assertEqual( str(context.exception), f"Error fetching Binance market snapshot for {self.trading_pair}.")
def test_existing_account_position_detected_on_positions_update(self, req_mock): url = utils.rest_url( CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2 ) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.async_run_with_timeout(task) self.assertEqual(len(self.exchange.account_positions), 1) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.trading_pair.replace("-", ""), self.symbol)
def test_get_funding_info_from_exchange_error_response(self, mock_api): url = utils.rest_url(CONSTANTS.MARK_PRICE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) result = self.async_run_with_timeout( self.data_source._get_funding_info_from_exchange( self.trading_pair)) self.assertIsNone(result) self._is_logged( "ERROR", f"Unable to fetch FundingInfo for {self.trading_pair}. Error: None" )
def test_get_last_traded_prices(self, mock_api): url = utils.rest_url(path_url=CONSTANTS.TICKER_PRICE_CHANGE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { # Truncated responses "lastPrice": "10.0", } mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], domain=self.domain)) self.assertTrue(self.trading_pair in result) self.assertEqual(10.0, result[self.trading_pair])
async def ping_listen_key(self, listen_key: str) -> bool: async with aiohttp.ClientSession() as client: async with self._throttler.execute_task( limit_id=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT): response: aiohttp.ClientResponse = await client.put( url=utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, self._domain), headers={"X-MBX-APIKEY": self._api_key}, params={"listenKey": listen_key}) data: Tuple[str, Any] = await response.json() if "code" in data: self.logger().warning( f"Failed to refresh the listen key {listen_key}: {data}" ) return False return True
def test_listen_for_order_book_snapshots_cancelled_error_raised( self, mock_api): url = utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) self.assertEqual(0, msg_queue.qsize())