def test_bitfinex_exchange_assets_are_known(mock_bitfinex): """This tests only exchange (trades) assets (not margin, nor futures ones). """ currencies_response = mock_bitfinex._query_currencies() if currencies_response.success is False: response = currencies_response.response test_warnings.warn( UserWarning( f'Failed to request {mock_bitfinex.name} currencies list. ' f'Response status code: {response.status_code}. ' f'Response text: {response.text}. Xfailing this test', )) pytest.xfail('Failed to request {mock_bitfinex.name} currencies list') exchange_pairs_response = mock_bitfinex._query_exchange_pairs() if exchange_pairs_response.success is False: response = exchange_pairs_response.response test_warnings.warn( UserWarning( f'Failed to request {mock_bitfinex.name} exchange pairs list. ' f'Response status code: {response.status_code}. ' f'Response text: {response.text}. Xfailing this test', )) pytest.xfail( 'Failed to request {mock_bitfinex.name} exchange pairs list') currency_map_response = mock_bitfinex._query_currency_map() if currency_map_response.success is False: response = currency_map_response.response test_warnings.warn( UserWarning( f'Failed to request {mock_bitfinex.name} currency map. ' f'Response status code: {response.status_code}. ' f'Response text: {response.text}. Xfailing this test', )) pytest.xfail('Failed to request {mock_bitfinex.name} currency map') test_assets = set(BITFINEX_EXCHANGE_TEST_ASSETS) unsupported_assets = set(UNSUPPORTED_BITFINEX_ASSETS) currency_map = currency_map_response.currency_map symbols = set() for symbol in currencies_response.currencies: if symbol in test_assets: continue for pair in exchange_pairs_response.pairs: if pair.startswith(symbol) or pair.endswith(symbol): symbols.add(symbol) break for symbol in symbols: try: asset_from_bitfinex( bitfinex_name=symbol, currency_map=currency_map, ) except UnsupportedAsset: assert symbol in unsupported_assets except UnknownAsset as e: test_warnings.warn( UserWarning( f'Found unknown asset {e.asset_name} in {mock_bitfinex.name}. ' f'Support for it has to be added', ))
def _deserialize_trade(self, raw_result: List[Any]) -> Trade: """Process a trade result from Bitfinex and deserialize it. The base and quote assets are instantiated using the `fee_currency_symbol` (from raw_result[10]) over the pair (from raw_result[1]). Known pairs format: 'tETHUST', 'tETH:UST'. Can raise DeserializationError. Schema reference in: https://docs.bitfinex.com/reference#rest-auth-trades """ amount = deserialize_asset_amount(raw_result[4]) trade_type = TradeType.BUY if amount >= ZERO else TradeType.SELL bfx_pair = self._process_bfx_pair(raw_result[1]) if bfx_pair not in self.pair_bfx_symbols_map: raise DeserializationError( f'Could not deserialize bitfinex trade pair {raw_result[1]}. ' f'Raw trade: {raw_result}', ) bfx_base_asset_symbol, bfx_quote_asset_symbol = self.pair_bfx_symbols_map[ bfx_pair] try: base_asset = asset_from_bitfinex( bitfinex_name=bfx_base_asset_symbol, currency_map=self.currency_map, ) quote_asset = asset_from_bitfinex( bitfinex_name=bfx_quote_asset_symbol, currency_map=self.currency_map, ) fee_asset = asset_from_bitfinex( bitfinex_name=raw_result[10], currency_map=self.currency_map, ) except (UnknownAsset, UnsupportedAsset) as e: asset_tag = 'Unknown' if isinstance( e, UnknownAsset) else 'Unsupported' raise DeserializationError( f'{asset_tag} {e.asset_name} found while processing trade pair due to: {str(e)}', ) from e trade = Trade( timestamp=Timestamp(int(raw_result[2] / 1000)), location=Location.BITFINEX, base_asset=base_asset, quote_asset=quote_asset, trade_type=trade_type, amount=AssetAmount(abs(amount)), rate=deserialize_price(raw_result[5]), fee=Fee(abs(deserialize_fee(raw_result[9]))), fee_currency=fee_asset, link=str(raw_result[0]), notes='', ) return trade
def _deserialize_asset_movement(self, raw_result: List[Any]) -> AssetMovement: """Process an asset movement (i.e. deposit or withdrawal) from Bitfinex and deserialize it. Bitfinex support confirmed the following in regards to DESTINATION_ADDRESS and TRANSACTION_ID: - Fiat movement: both attributes won't show any value. - Cryptocurrency movement: address and tx id on the blockchain. Timestamp is from MTS_STARTED (when the movement was created), and not from MTS_UPDATED (when it was completed/cancelled). Can raise DeserializationError. Schema reference in: https://docs.bitfinex.com/reference#rest-auth-movements """ if raw_result[9] != 'COMPLETED': raise DeserializationError( f'Unexpected bitfinex movement with status: {raw_result[5]}. ' f'Only completed movements are processed. Raw movement: {raw_result}', ) try: fee_asset = asset_from_bitfinex( bitfinex_name=raw_result[1], currency_map=self.currency_map, ) except (UnknownAsset, UnsupportedAsset) as e: asset_tag = 'Unknown' if isinstance(e, UnknownAsset) else 'Unsupported' raise DeserializationError( f'{asset_tag} {e.asset_name} found while processing movement asset ' f'due to: {str(e)}', ) from e amount = deserialize_asset_amount(raw_result[12]) category = ( AssetMovementCategory.DEPOSIT if amount > ZERO else AssetMovementCategory.WITHDRAWAL ) address = None transaction_id = None if fee_asset.is_fiat() is False: address = str(raw_result[16]) transaction_id = str(raw_result[20]) asset_movement = AssetMovement( timestamp=Timestamp(int(raw_result[5] / 1000)), location=Location.BITFINEX, category=category, address=address, transaction_id=transaction_id, asset=fee_asset, amount=abs(amount), fee_asset=fee_asset, fee=Fee(abs(deserialize_fee(raw_result[13]))), link=str(raw_result[0]), ) return asset_movement
def query_balances(self) -> ExchangeQueryBalances: """Return the account exchange balances on Bitfinex The wallets endpoint returns a list where each item is a currency wallet. Each currency wallet has type (i.e. exchange, margin, funding), currency, balance, etc. Currencies (tickers) are in Bitfinex format and must be standardized. Endpoint documentation: https://docs.bitfinex.com/reference#rest-auth-wallets """ self.first_connection() response = self._api_query('wallets') if response.status_code != HTTPStatus.OK: result, msg = self._process_unsuccessful_response( response=response, case='balances', ) return result, msg try: response_list = jsonloads_list(response.text) except JSONDecodeError as e: msg = f'{self.name} returned invalid JSON response: {response.text}.' log.error(msg) raise RemoteError(msg) from e # Wallet items indices currency_index = 1 balance_index = 2 assets_balance: DefaultDict[Asset, Balance] = defaultdict(Balance) for wallet in response_list: if len(wallet) < API_WALLET_MIN_RESULT_LENGTH: log.error( f'Error processing a {self.name} balance result. ' f'Found less items than expected', wallet=wallet, ) self.msg_aggregator.add_error( f'Failed to deserialize a {self.name} balance result. ' f'Check logs for details. Ignoring it.', ) continue if wallet[balance_index] <= 0: continue # bitfinex can show small negative balances for some coins. Ignore try: asset = asset_from_bitfinex( bitfinex_name=wallet[currency_index], currency_map=self.currency_map, ) except (UnknownAsset, UnsupportedAsset) as e: asset_tag = 'unknown' if isinstance(e, UnknownAsset) else 'unsupported' self.msg_aggregator.add_warning( f'Found {asset_tag} {self.name} asset {e.asset_name} due to: {str(e)}. ' f'Ignoring its balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset=asset) except RemoteError as e: self.msg_aggregator.add_error( f'Error processing {self.name} {asset.name} balance result due to inability ' f'to query USD price: {str(e)}. Skipping balance result.', ) continue try: amount = deserialize_asset_amount(wallet[balance_index]) except DeserializationError as e: self.msg_aggregator.add_error( f'Error processing {self.name} {asset.name} balance result due to inability ' f'to deserialize asset amount due to {str(e)}. Skipping balance result.', ) continue assets_balance[asset] += Balance( amount=amount, usd_value=amount * usd_price, ) return dict(assets_balance), ''