Example #1
0
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', ))
Example #2
0
    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}'),
        )
Example #3
0
    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
Example #4
0
    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, ''