def test_bitstamp_exchange_assets_are_known(mock_bitstamp): request_url = f'{mock_bitstamp.base_uri}/v2/trading-pairs-info' try: response = requests.get(request_url) except requests.exceptions.RequestException as e: raise RemoteError( f'Bitstamp get request at {request_url} connection error: {str(e)}.', ) from e if response.status_code != 200: raise RemoteError( f'Bitstamp query responded with error status code: {response.status_code} ' f'and text: {response.text}', ) try: response_list = jsonloads_list(response.text) except JSONDecodeError as e: raise RemoteError( f'Bitstamp returned invalid JSON response: {response.text}') from e # Extract the unique symbols from the exchange pairs pairs = [raw_result.get('name') for raw_result in response_list] symbols = set() for pair in pairs: symbols.update(set(pair.split('/'))) for symbol in symbols: try: asset_from_bitstamp(symbol) except UnknownAsset as e: test_warnings.warn( UserWarning( f'Found unknown asset {e.asset_name} in {mock_bitstamp.name}. ' f'Support for it has to be added', ))
def _get_trade_pair_data_from_transaction( raw_result: Dict[str, Any]) -> TradePairData: """Given a user transaction that contains the base and quote assets' symbol as keys, return the Bitstamp trade pair data (raw pair str, base/quote assets raw symbols, and TradePair). NB: any custom pair conversion (e.g. from Bitstamp asset symbol to world) should happen here. Can raise DeserializationError. """ try: pair = [ key for key in raw_result.keys() if '_' in key and key != 'order_id' ][0] except IndexError as e: raise DeserializationError( 'Could not deserialize Bitstamp trade pair from user transaction. ' f'Trade pair not found in: {raw_result}.', ) from e # NB: `pair_get_assets()` is not used for simplifying the calls and # storing the raw pair strings. base_asset_symbol, quote_asset_symbol = pair.split('_') try: base_asset = asset_from_bitstamp(base_asset_symbol) quote_asset = asset_from_bitstamp(quote_asset_symbol) except (UnknownAsset, UnsupportedAsset) as e: log.error(str(e)) asset_tag = 'Unknown' if isinstance( e, UnknownAsset) else 'Unsupported' raise DeserializationError( f'{asset_tag} {e.asset_name} found while processing trade pair.', ) from e return TradePairData( pair=pair, base_asset_symbol=base_asset_symbol, quote_asset_symbol=quote_asset_symbol, base_asset=base_asset, quote_asset=quote_asset, trade_pair=TradePair( f'{base_asset.identifier}_{quote_asset.identifier}'), )
def _deserialize_asset_movement( raw_movement: Dict[str, Any], ) -> AssetMovement: """Process a deposit/withdrawal user transaction from Bitstamp and deserialize it. Can raise DeserializationError. From Bitstamp documentation, deposits/withdrawals can have a fee (the amount is expected to be in the currency involved) https://www.bitstamp.net/fee-schedule/ Endpoint docs: https://www.bitstamp.net/api/#user-transactions """ type_ = deserialize_int_from_str(raw_movement['type'], 'bitstamp asset movement') category: AssetMovementCategory if type_ == 0: category = AssetMovementCategory.DEPOSIT elif type_ == 1: category = AssetMovementCategory.WITHDRAWAL else: raise AssertionError( f'Unexpected Bitstamp asset movement case: {type_}.') timestamp = deserialize_timestamp_from_bitstamp_date( raw_movement['datetime']) amount: FVal fee_asset: Asset for symbol in BITSTAMP_ASSET_MOVEMENT_SYMBOLS: amount = deserialize_asset_amount(raw_movement.get(symbol, '0')) if amount != ZERO: fee_asset = asset_from_bitstamp(symbol) break if amount == ZERO: raise DeserializationError( 'Could not deserialize Bitstamp asset movement from user transaction. ' f'Unexpected asset amount combination found in: {raw_movement}.', ) asset_movement = AssetMovement( timestamp=timestamp, location=Location.BITSTAMP, category=category, address=None, # requires query "crypto_transactions" endpoint transaction_id=None, # requires query "crypto_transactions" endpoint asset=fee_asset, amount=abs(amount), fee_asset=fee_asset, fee=deserialize_fee(raw_movement['fee']), link=str(raw_movement['id']), ) return asset_movement
def query_balances(self) -> Tuple[Optional[Dict[Asset, Dict[str, FVal]]], str]: """Return the account balances on Bistamp The balance endpoint returns a dict where the keys (str) are related to assets and the values (str) amounts. The keys that end with `_balance` contain the exact amount of an asset the account is holding (available amount + orders amount, per asset). """ response = self._api_query('balance') if response.status_code != HTTPStatus.OK: result, msg = self._process_unsuccessful_response( response=response, case='balances', ) return result, msg try: response_dict = rlk_jsonloads_dict(response.text) except JSONDecodeError as e: msg = f'Bitstamp returned invalid JSON response: {response.text}.' log.error(msg) raise RemoteError(msg) from e asset_balance: Dict[Asset, Dict[str, FVal]] = {} for entry, amount in response_dict.items(): amount = FVal(amount) if not entry.endswith('_balance') or amount == ZERO: continue symbol = entry.split('_')[0] # If no `_`, defaults to entry try: asset = asset_from_bitstamp(symbol) except DeserializationError as e: log.error( 'Error processing a Bitstamp balance.', entry=entry, error=str(e), ) self.msg_aggregator.add_error( 'Failed to deserialize a Bitstamp balance. ' 'Check logs for details. Ignoring it.', ) continue except (UnknownAsset, UnsupportedAsset) as e: log.error(str(e)) asset_tag = 'unknown' if isinstance(e, UnknownAsset) else 'unsupported' self.msg_aggregator.add_warning( f'Found {asset_tag} Bistamp asset {e.asset_name}. Ignoring its balance query.', ) continue try: usd_price = Inquirer().find_usd_price(asset=asset) except RemoteError as e: log.error(str(e)) self.msg_aggregator.add_error( f'Error processing Bitstamp balance result due to inability to ' f'query USD price: {str(e)}. Skipping balance entry.', ) continue asset_balance[asset] = { 'amount': amount, 'usd_value': amount * usd_price, } return asset_balance, ''