Example #1
0
    def set_main_currency(self, given_currency: str) -> None:
        currency = Asset(given_currency)
        msg = 'main currency checks should have happened at rotkehlchen.set_settings()'
        assert currency.is_fiat(), msg

        self.profit_currency = currency
        self.events.profit_currency = currency
Example #2
0
    def add_sell_and_corresponding_buy(
        self,
        selling_asset: Asset,
        selling_amount: FVal,
        receiving_asset: Asset,
        receiving_amount: FVal,
        gain_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        trade_rate: FVal,
        rate_in_profit_currency: FVal,
        timestamp: Timestamp,
    ) -> None:
        """
        Add and process a selling event to the events list

        Args:
            selling_asset (str): The ticker representation of the asset we sell.
            selling_amount (FVal): The amount of `selling_asset` for sale.
            receiving_asset (str): The ticker representation of the asset we receive
                                   in exchange for `selling_asset`.
            receiving_amount (FVal): The amount of `receiving_asset` we receive.
            gain_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent
                                            we receive after doing this trade. Fees are not counted
                                            in this.
            total_fee_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent
                                                 we pay in fees after doing this trade.
            trade_rate (FVal): How much does 1 unit of `receiving_asset` cost in `selling_asset`
            rate_in_profit_currency (FVal): The equivalent of `trade_rate` in `profit_currency`
            timestamp (int): The timestamp for the trade
        """
        self.add_sell(
            selling_asset,
            selling_amount,
            receiving_asset,
            receiving_amount,
            gain_in_profit_currency,
            total_fee_in_profit_currency,
            trade_rate,
            rate_in_profit_currency,
            timestamp,
            is_virtual=False,
        )

        if receiving_asset.is_fiat() or not self.include_crypto2crypto:
            return

        log.debug(
            f'Selling {selling_asset} for {receiving_asset} also introduces a virtual buy event',
        )
        # else then you are also buying some other asset through your sell
        self.add_buy(
            bought_asset=receiving_asset,
            bought_amount=receiving_amount,
            paid_with_asset=selling_asset,
            trade_rate=1 / trade_rate,
            fee_in_profit_currency=total_fee_in_profit_currency,
            fee_currency=receiving_asset,  # does not matter
            timestamp=timestamp,
            is_virtual=True,
        )
Example #3
0
    def query_historical_price(from_asset: Asset, to_asset: Asset, timestamp: Timestamp) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        Args:
            from_asset: The ticker symbol of the asset for which we want to know
                        the price.
            to_asset: The ticker symbol of the asset against which we want to
                      know the price.
            timestamp: The timestamp at which to query the price

        May raise:
        - PriceQueryUnknownFromAsset if the from asset is known to miss from cryptocompare
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from the external service.
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        log.debug(
            'Querying historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )

        if from_asset == to_asset:
            return Price(FVal('1'))

        if from_asset.is_fiat() and to_asset.is_fiat():
            # if we are querying historical forex data then try something other than cryptocompare
            price = Inquirer().query_historical_fiat_exchange_rates(
                from_fiat_currency=from_asset,
                to_fiat_currency=to_asset,
                timestamp=timestamp,
            )
            if price is not None:
                return price
            # else cryptocompare also has historical fiat to fiat data

        instance = PriceHistorian()
        return instance._cryptocompare.query_historical_price(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            historical_data_start=instance._historical_data_start,
        )
Example #4
0
    def set_main_currency(self, currency: Asset) -> None:
        if not currency.is_fiat():
            raise ValueError(
                f'Attempted to set unsupported "{str(currency)}" as main currency.',
            )

        self.profit_currency = currency
        self.events.profit_currency = currency
Example #5
0
    def query_historical_fiat_exchange_rates(
        from_fiat_currency: Asset,
        to_fiat_currency: Asset,
        timestamp: Timestamp,
    ) -> Optional[Price]:
        assert from_fiat_currency.is_fiat(
        ), 'fiat currency should have been provided'
        assert to_fiat_currency.is_fiat(
        ), 'fiat currency should have been provided'
        # Check cache
        price_cache_entry = GlobalDBHandler().get_historical_price(
            from_asset=from_fiat_currency,
            to_asset=to_fiat_currency,
            timestamp=timestamp,
            max_seconds_distance=DAY_IN_SECONDS,
        )
        if price_cache_entry:
            return price_cache_entry.price

        try:
            prices_map = get_historical_xratescom_exchange_rates(
                from_asset=from_fiat_currency,
                time=timestamp,
            )
        except RemoteError:
            return None

        # Since xratecoms has daily rates let's save at timestamp of UTC day start
        for asset, asset_price in prices_map.items():
            GlobalDBHandler().add_historical_prices(entries=[
                HistoricalPrice(
                    from_asset=from_fiat_currency,
                    to_asset=asset,
                    source=HistoricalPriceOracle.XRATESCOM,
                    timestamp=timestamp_to_daystart_timestamp(timestamp),
                    price=asset_price,
                )
            ])
            if asset == to_fiat_currency:
                rate = asset_price

        log.debug('Historical fiat exchange rate query succesful', rate=rate)
        return rate
Example #6
0
    def _save_forex_rate(
            date: str,
            from_currency: Asset,
            to_currency: Asset,
            price: FVal,
    ) -> None:
        assert from_currency.is_fiat(), 'fiat currency should have been provided'
        assert to_currency.is_fiat(), 'fiat currency should have been provided'
        instance = Inquirer()
        if date not in instance._cached_forex_data:
            instance._cached_forex_data[date] = {}

        if from_currency not in instance._cached_forex_data[date]:
            instance._cached_forex_data[date][from_currency] = {}

        msg = 'Cached value should not already exist'
        assert to_currency not in instance._cached_forex_data[date][from_currency], msg
        instance._cached_forex_data[date][from_currency][to_currency] = price
        instance.save_historical_forex_data()
Example #7
0
 def add_fiat_balance(self, currency: Asset, amount: FVal) -> None:
     assert currency.is_fiat()
     cursor = self.conn.cursor()
     # We don't care about previous value so this simple insert or replace should work
     cursor.execute(
         'INSERT OR REPLACE INTO current_balances(asset, amount) VALUES (?, ?)',
         (currency.identifier, str(amount)),
     )
     self.conn.commit()
     self.update_last_write()
Example #8
0
    def query_historical_price(from_asset: Asset, to_asset: Asset,
                               timestamp: Timestamp) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        Args:
            from_asset: The ticker symbol of the asset for which we want to know
                        the price.
            to_asset: The ticker symbol of the asset against which we want to
                      know the price.
            timestamp: The timestamp at which to query the price
        """
        log.debug(
            'Querying historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )

        if from_asset == to_asset:
            return Price(FVal('1'))

        if from_asset.is_fiat() and to_asset.is_fiat():
            # if we are querying historical forex data then try something other than cryptocompare
            price = Inquirer().query_historical_fiat_exchange_rates(
                from_fiat_currency=FiatAsset(from_asset.identifier),
                to_fiat_currency=FiatAsset(to_asset.identifier),
                timestamp=timestamp,
            )
            if price is not None:
                return price
            # else cryptocompare also has historical fiat to fiat data

        instance = PriceHistorian()
        return instance._cryptocompare.query_historical_price(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            historical_data_start=instance._historical_data_start,
        )
Example #9
0
def _query_currency_converterapi(base: Asset, quote: Asset) -> Optional[Price]:
    assert base.is_fiat(), 'fiat currency should have been provided'
    assert quote.is_fiat(), 'fiat currency should have been provided'
    log.debug(
        'Query free.currencyconverterapi.com fiat pair',
        base_currency=base.identifier,
        quote_currency=quote.identifier,
    )
    pair = f'{base.identifier}_{quote.identifier}'
    querystr = (f'https://free.currconv.com/api/v7/convert?'
                f'q={pair}&compact=ultra&apiKey={CURRENCYCONVERTER_API_KEY}')
    try:
        resp = request_get_dict(querystr)
        return Price(FVal(resp[pair]))
    except (ValueError, RemoteError, KeyError, UnableToDecryptRemoteData):
        log.error(
            'Querying free.currencyconverterapi.com fiat pair failed',
            base_currency=base.identifier,
            quote_currency=quote.identifier,
        )
        return None
Example #10
0
    def find_usd_price(
        asset: Asset,
        ignore_cache: bool = False,
    ) -> Price:
        """Returns the current USD price of the asset

        Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs
        """
        if asset == A_USD:
            return Price(FVal(1))

        instance = Inquirer()
        cache_key = (asset, A_USD)
        if ignore_cache is False:
            cache = instance.get_cached_current_price_entry(
                cache_key=cache_key)
            if cache is not None:
                return cache.price

        if asset.is_fiat():
            try:
                return instance._query_fiat_pair(base=asset, quote=A_USD)
            except RemoteError:
                pass  # continue, a price can be found by one of the oracles (CC for example)

        if asset in instance.special_tokens:
            ethereum = instance._ethereum
            assert ethereum, 'Inquirer should never be called before the injection of ethereum'
            token = EthereumToken.from_asset(asset)
            assert token, 'all assets in special tokens are already ethereum tokens'
            underlying_asset_price = get_underlying_asset_price(token)
            usd_price = handle_defi_price_query(
                ethereum=ethereum,
                token=token,
                underlying_asset_price=underlying_asset_price,
            )
            if usd_price is None:
                price = Price(ZERO)
            else:
                price = Price(usd_price)

            Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                price=price, time=ts_now())  # noqa: E501
            return price

        return instance._query_oracle_instances(from_asset=asset,
                                                to_asset=A_USD)
Example #11
0
    def set_settings(self, settings: Dict[str, Any]) -> Tuple[bool, str]:
        log.info('Add new settings')

        message = ''
        with self.lock:
            if 'eth_rpc_endpoint' in settings:
                result, msg = self.blockchain.set_eth_rpc_endpoint(
                    settings['eth_rpc_endpoint'])
                if not result:
                    # Don't save it in the DB
                    del settings['eth_rpc_endpoint']
                    message += "\nEthereum RPC endpoint not set: " + msg

            if 'main_currency' in settings:
                given_symbol = settings['main_currency']
                try:
                    main_currency = Asset(given_symbol)
                except UnknownAsset:
                    return False, f'Unknown fiat currency {given_symbol} provided'
                except DeserializationError:
                    return False, 'Non string type given for fiat currency'

                if not main_currency.is_fiat():
                    msg = (
                        f'Provided symbol for main currency {given_symbol} is '
                        f'not a fiat currency')
                    return False, msg

                if main_currency != A_USD:
                    self.usd_to_main_currency_rate = Inquirer(
                    ).query_fiat_pair(
                        A_USD,
                        main_currency,
                    )

            res, msg = self.accountant.customize(settings)
            if not res:
                message += '\n' + msg
                return False, message

            _, msg, = self.data.set_settings(settings, self.accountant)
            if msg != '':
                message += '\n' + msg

            # Always return success here but with a message
            return True, message
Example #12
0
    def set_main_currency(self, currency_string: str) -> Tuple[bool, str]:
        """Takes a currency string from the API and sets it as the main currency for rotki

        Returns True and empty string for success and False and error string for error
        """
        try:
            currency = Asset(currency_string)
        except UnknownAsset:
            msg = f'An unknown asset {currency_string} was given for main currency'
            log.critical(msg)
            return False, msg

        if not currency.is_fiat():
            msg = f'A non-fiat asset {currency_string} was given for main currency'
            log.critical(msg)
            return False, msg

        fiat_currency = FiatAsset(currency.identifier)
        with self.lock:
            self.data.set_main_currency(fiat_currency, self.accountant)

        return True, ''
Example #13
0
def get_asset_balance_total(asset_symbol: str, setup: BalancesTestSetup) -> FVal:
    conversion_function = satoshis_to_btc if asset_symbol == 'BTC' else from_wei
    total = ZERO
    asset = Asset(asset_symbol)
    if asset.is_fiat():
        total += setup.fiat_balances.get(asset_symbol, ZERO)
    elif asset_symbol in ('ETH', 'BTC'):
        asset_balances = getattr(setup, f'{asset_symbol.lower()}_balances')
        total += sum(conversion_function(FVal(b)) for b in asset_balances)
    elif EthereumToken(asset_symbol):
        asset_balances = setup.token_balances[EthereumToken(asset_symbol)]
        total += sum(conversion_function(FVal(b)) for b in asset_balances)
    else:
        raise AssertionError(f'not implemented for asset {asset_symbol}')
    total += setup.binance_balances.get(asset_symbol, ZERO)
    total += setup.poloniex_balances.get(asset_symbol, ZERO)

    if setup.manually_tracked_balances:
        for entry in setup.manually_tracked_balances:
            if entry.asset.identifier == asset_symbol:
                total += entry.amount

    return total
Example #14
0
    def query_historical_price(
        self,
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
    ) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        This tries to:
        1. Find cached cryptocompare values and return them
        2. If none exist at the moment try the normal historical price endpoint
        3. Else fail

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is known to miss from cryptocompare
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the cryptocompare server
        or with reading the response returned by the server
        """
        # NB: check if the from..to asset price (or viceversa) is a special
        # histohour API case.
        price = self._check_and_get_special_histohour_price(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )
        if price != Price(ZERO):
            return price

        try:
            data = self.get_historical_data(
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
                only_check_cache=True,
            )
        except UnsupportedAsset as e:
            raise PriceQueryUnsupportedAsset(e.asset_name) from e

        price = self._retrieve_price_from_data(
            data=data,
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )
        if price == Price(ZERO):
            log.debug(
                f"Couldn't find historical price from {from_asset} to "
                f"{to_asset} at timestamp {timestamp} through cryptocompare."
                f" Attempting to get daily price...", )
            price = self.query_endpoint_pricehistorical(
                from_asset, to_asset, timestamp)

        comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD)
                                     or (from_asset.is_fiat()
                                         and from_asset != A_USD))
        if comparison_to_nonusd_fiat:
            price = self._adjust_to_cryptocompare_price_incosistencies(
                price=price,
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
            )

        if price == Price(ZERO):
            raise NoPriceForGivenTimestamp(
                from_asset=from_asset,
                to_asset=to_asset,
                date=timestamp_to_date(timestamp,
                                       formatstr='%d/%m/%Y, %H:%M:%S'),
            )

        log.debug(
            'Got historical price from cryptocompare',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            price=price,
        )

        return price
Example #15
0
    def add_buy_and_corresponding_sell(
        self,
        bought_asset: Asset,
        bought_amount: FVal,
        paid_with_asset: Asset,
        trade_rate: FVal,
        fee_in_profit_currency: Fee,
        fee_currency: Asset,
        timestamp: Timestamp,
    ) -> None:
        self.add_buy(
            bought_asset=bought_asset,
            bought_amount=bought_amount,
            paid_with_asset=paid_with_asset,
            trade_rate=trade_rate,
            fee_in_profit_currency=fee_in_profit_currency,
            fee_currency=fee_currency,
            timestamp=timestamp,
            is_virtual=False,
        )

        if paid_with_asset.is_fiat() or not self.include_crypto2crypto:
            return

        # else you are also selling some other asset to buy the bought asset
        log.debug(
            f'Buying {bought_asset} with {paid_with_asset} also introduces a virtual sell event',
        )
        try:
            bought_asset_rate_in_profit_currency = self.get_rate_in_profit_currency(
                bought_asset,
                timestamp,
            )
        except (NoPriceForGivenTimestamp, PriceQueryUnknownFromAsset):
            bought_asset_rate_in_profit_currency = FVal(-1)

        if bought_asset_rate_in_profit_currency != FVal(-1):
            # The asset bought does not have a price yet
            # Can happen for Token sales, presales e.t.c.
            with_bought_asset_gain = bought_asset_rate_in_profit_currency * bought_amount
            receiving_asset = bought_asset
            receiving_amount = bought_amount
            rate_in_profit_currency = bought_asset_rate_in_profit_currency / trade_rate
            gain_in_profit_currency = with_bought_asset_gain

        sold_amount = trade_rate * bought_amount

        sold_asset_rate_in_profit_currency = self.get_rate_in_profit_currency(
            paid_with_asset,
            timestamp,
        )
        with_sold_asset_gain = sold_asset_rate_in_profit_currency * sold_amount

        # Consider as value of the sell what would give the least profit
        if (bought_asset_rate_in_profit_currency == -1
                or with_sold_asset_gain < with_bought_asset_gain):
            receiving_asset = self.profit_currency
            receiving_amount = with_sold_asset_gain
            trade_rate = sold_asset_rate_in_profit_currency
            rate_in_profit_currency = sold_asset_rate_in_profit_currency
            gain_in_profit_currency = with_sold_asset_gain

        self.add_sell(
            selling_asset=paid_with_asset,
            selling_amount=sold_amount,
            receiving_asset=receiving_asset,
            receiving_amount=receiving_amount,
            trade_rate=trade_rate,
            rate_in_profit_currency=rate_in_profit_currency,
            gain_in_profit_currency=gain_in_profit_currency,
            total_fee_in_profit_currency=fee_in_profit_currency,
            timestamp=timestamp,
            is_virtual=True,
        )
Example #16
0
    def query_historical_price(
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
    ) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        Args:
            from_asset: The ticker symbol of the asset for which we want to know
                        the price.
            to_asset: The ticker symbol of the asset against which we want to
                      know the price.
            timestamp: The timestamp at which to query the price

        May raise:
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from the external service.
        """
        log.debug(
            'Querying historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )
        if from_asset == to_asset:
            return Price(FVal('1'))

        # Querying historical forex data is attempted first via exchangerates API,
        # and then via any price oracle that has fiat to fiat.
        if from_asset.is_fiat() and to_asset.is_fiat():
            price = Inquirer().query_historical_fiat_exchange_rates(
                from_fiat_currency=from_asset,
                to_fiat_currency=to_asset,
                timestamp=timestamp,
            )
            if price is not None:
                return price
            # else cryptocompare also has historical fiat to fiat data

        instance = PriceHistorian()
        oracles = instance._oracles
        oracle_instances = instance._oracle_instances
        assert isinstance(oracles, list) and isinstance(
            oracle_instances, list
        ), ('PriceHistorian should never be called before the setting the oracles'
            )
        for oracle, oracle_instance in zip(oracles, oracle_instances):
            can_query_history = oracle_instance.can_query_history(
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
            )
            if can_query_history is False:
                continue

            try:
                price = oracle_instance.query_historical_price(
                    from_asset=from_asset,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
            except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp,
                    RemoteError) as e:
                log.warning(
                    f'Historical price oracle {oracle} failed to request '
                    f'due to: {str(e)}.',
                    from_asset=from_asset,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
                continue

            if price != Price(ZERO):
                log.debug(
                    f'Historical price oracle {oracle} got price',
                    price=price,
                    from_asset=from_asset,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
                return price

        raise NoPriceForGivenTimestamp(
            from_asset=from_asset,
            to_asset=to_asset,
            date=timestamp_to_date(timestamp,
                                   formatstr='%d/%m/%Y, %H:%M:%S',
                                   treat_as_local=True),
        )
Example #17
0
def _scrape_xratescom_exchange_rates(url: str) -> Dict[Asset, Price]:
    """
    Scrapes x-rates.com website for the exchange rates tables

    May raise:
    - RemoteError if we can't query x-rates.com
    """
    log.debug(f'Querying x-rates.com stats: {url}')
    prices = {}
    try:
        response = requests.get(url=url, timeout=DEFAULT_TIMEOUT_TUPLE)
    except requests.exceptions.RequestException as e:
        raise RemoteError(f'x-rates.com request {url} failed due to {str(e)}') from e

    if response.status_code != 200:
        raise RemoteError(
            f'x-rates.com request {url} failed with code: {response.status_code}'
            f' and response: {response.text}',
        )

    soup = BeautifulSoup(
        response.text,
        'html.parser',
        parse_only=SoupStrainer('table', {'class': 'tablesorter ratesTable'}),
    )
    if soup is None:
        raise RemoteError('Could not find <table> while parsing x-rates stats page')
    try:
        tr = soup.table.tbody.tr
    except AttributeError as e:
        raise RemoteError('Could not find first <tr> while parsing x-rates.com page') from e

    while tr is not None:
        secondtd = tr.select('td:nth-of-type(2)')[0]
        try:
            href = secondtd.a['href']
        except (AttributeError, KeyError) as e:
            raise RemoteError('Could not find a href of 2nd td while parsing x-rates.com page') from e  # noqa: E501

        parts = href.split('to=')
        if len(parts) != 2:
            raise RemoteError(f'Could not find to= in {href} while parsing x-rates.com page')

        try:
            to_asset = Asset(parts[1])
            if not to_asset.is_fiat():
                raise ValueError
        except (UnknownAsset, ValueError):
            log.debug(f'Skipping {parts[1]} asset because its not a known fiat asset while parsing x-rates.com page')  # noqa: E501
            tr = tr.find_next_sibling()
            continue

        try:
            price = deserialize_price(secondtd.a.text)
        except DeserializationError as e:
            log.debug(f'Could not parse x-rates.com rate of {to_asset.identifier} due to {str(e)}. Skipping ...')  # noqa: E501
            tr = tr.find_next_sibling()
            continue

        prices[to_asset] = price
        tr = tr.find_next_sibling()

    return prices
Example #18
0
    def add_buy(
        self,
        location: Location,
        bought_asset: Asset,
        bought_amount: FVal,
        paid_with_asset: Asset,
        trade_rate: FVal,
        fee_in_profit_currency: Fee,
        fee_currency: Asset,
        timestamp: Timestamp,
        is_virtual: bool = False,
    ) -> None:
        """
        Account for the given buy

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from all price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        skip_trade = (not self.include_crypto2crypto
                      and not bought_asset.is_fiat()
                      and not paid_with_asset.is_fiat())
        if skip_trade:
            return

        paid_with_asset_rate = self.get_rate_in_profit_currency(
            paid_with_asset, timestamp)
        buy_rate = paid_with_asset_rate * trade_rate

        self.handle_prefork_asset_buys(
            location=location,
            bought_asset=bought_asset,
            bought_amount=bought_amount,
            paid_with_asset=paid_with_asset,
            trade_rate=trade_rate,
            fee_in_profit_currency=fee_in_profit_currency,
            fee_currency=fee_currency,
            timestamp=timestamp,
        )

        if bought_asset not in self.events:
            self.events[bought_asset] = Events([], [])

        gross_cost = bought_amount * buy_rate
        cost_in_profit_currency = gross_cost + fee_in_profit_currency

        self.events[bought_asset].buys.append(
            BuyEvent(
                amount=bought_amount,
                timestamp=timestamp,
                rate=buy_rate,
                fee_rate=fee_in_profit_currency / bought_amount,
            ), )
        log.debug(
            'Buy Event',
            sensitive_log=True,
            location=str(location),
            bought_amount=bought_amount,
            bought_asset=bought_asset,
            paid_with_asset=paid_with_asset,
            rate=trade_rate,
            rate_in_profit_currency=buy_rate,
            profit_currency=self.profit_currency,
            timestamp=timestamp,
        )

        if timestamp >= self.query_start_ts:
            self.csv_exporter.add_buy(
                location=location,
                bought_asset=bought_asset,
                rate=buy_rate,
                fee_cost=fee_in_profit_currency,
                amount=bought_amount,
                cost=cost_in_profit_currency,
                paid_with_asset=paid_with_asset,
                paid_with_asset_rate=paid_with_asset_rate,
                timestamp=timestamp,
                is_virtual=is_virtual,
            )
Example #19
0
    def find_usd_price(
        asset: Asset,
        ignore_cache: bool = False,
    ) -> Price:
        """Returns the current USD price of the asset

        Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs
        """
        if asset == A_USD:
            return Price(FVal(1))

        instance = Inquirer()
        cache_key = (asset, A_USD)
        if ignore_cache is False:
            cache = instance.get_cached_current_price_entry(
                cache_key=cache_key)
            if cache is not None:
                return cache.price

        if asset.is_fiat():
            try:
                return instance._query_fiat_pair(base=asset, quote=A_USD)
            except RemoteError:
                pass  # continue, a price can be found by one of the oracles (CC for example)

        # Try and check if it is an ethereum token with specified protocol or underlying tokens
        is_known_protocol = False
        underlying_tokens = None
        try:
            token = EthereumToken.from_asset(asset)
            if token is not None:
                if token.protocol is not None:
                    is_known_protocol = token.protocol in KnownProtocolsAssets
                underlying_tokens = GlobalDBHandler(
                ).get_ethereum_token(  # type: ignore
                    token.ethereum_address, ).underlying_tokens
        except UnknownAsset:
            pass

        # Check if it is a special token
        if asset in instance.special_tokens:
            ethereum = instance._ethereum
            assert ethereum, 'Inquirer should never be called before the injection of ethereum'
            assert token, 'all assets in special tokens are already ethereum tokens'
            underlying_asset_price = get_underlying_asset_price(token)
            usd_price = handle_defi_price_query(
                ethereum=ethereum,
                token=token,
                underlying_asset_price=underlying_asset_price,
            )
            if usd_price is None:
                price = Price(ZERO)
            else:
                price = Price(usd_price)

            Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                price=price, time=ts_now())  # noqa: E501
            return price

        if is_known_protocol is True or underlying_tokens is not None:
            assert token is not None
            result = get_underlying_asset_price(token)
            if result is None:
                usd_price = Price(ZERO)
                if instance._ethereum is not None:
                    instance._ethereum.msg_aggregator.add_warning(
                        f'Could not find price for {token}', )
            else:
                usd_price = Price(result)
            Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                price=usd_price,
                time=ts_now(),
            )
            return usd_price

        # BSQ is a special asset that doesnt have oracle information but its custom API
        if asset == A_BSQ:
            try:
                price_in_btc = get_bisq_market_price(asset)
                btc_price = Inquirer().find_usd_price(A_BTC)
                usd_price = Price(price_in_btc * btc_price)
                Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                    price=usd_price,
                    time=ts_now(),
                )
                return usd_price
            except (RemoteError, DeserializationError) as e:
                msg = f'Could not find price for BSQ. {str(e)}'
                if instance._ethereum is not None:
                    instance._ethereum.msg_aggregator.add_warning(msg)
                return Price(BTC_PER_BSQ * price_in_btc)

        if asset == A_KFEE:
            # KFEE is a kraken special asset where 1000 KFEE = 10 USD
            return Price(FVal(0.01))

        return instance._query_oracle_instances(from_asset=asset,
                                                to_asset=A_USD)
Example #20
0
    def add_buy_and_corresponding_sell(
        self,
        location: Location,
        bought_asset: Asset,
        bought_amount: FVal,
        paid_with_asset: Asset,
        trade_rate: FVal,
        fee_in_profit_currency: Fee,
        timestamp: Timestamp,
    ) -> None:
        """
        Account for the given buy and the corresponding sell if it's a crypto to crypto

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        self.add_buy(
            location=location,
            bought_asset=bought_asset,
            bought_amount=bought_amount,
            paid_with_asset=paid_with_asset,
            trade_rate=trade_rate,
            fee_in_profit_currency=fee_in_profit_currency,
            timestamp=timestamp,
            is_virtual=False,
        )

        if paid_with_asset.is_fiat() or not self.include_crypto2crypto:
            return

        # else you are also selling some other asset to buy the bought asset
        log.debug(
            f'Buying {bought_asset} with {paid_with_asset} also introduces a virtual sell event',
        )
        try:
            bought_asset_rate_in_profit_currency = self.get_rate_in_profit_currency(
                bought_asset,
                timestamp,
            )
        except (NoPriceForGivenTimestamp, PriceQueryUnsupportedAsset):
            bought_asset_rate_in_profit_currency = FVal(-1)

        if bought_asset_rate_in_profit_currency != FVal(-1):
            # The asset bought does not have a price yet
            # Can happen for Token sales, presales e.t.c.
            with_bought_asset_gain = bought_asset_rate_in_profit_currency * bought_amount
            receiving_asset = bought_asset
            receiving_amount = bought_amount
            rate_in_profit_currency = bought_asset_rate_in_profit_currency / trade_rate
            gain_in_profit_currency = with_bought_asset_gain

        sold_amount = trade_rate * bought_amount
        if sold_amount == ZERO:
            logger.error(
                f'Not adding a virtual sell event. Could not calculate it from '
                f'trade_rate * bought_amount = {trade_rate} * {bought_amount}',
            )
            return

        sold_asset_rate_in_profit_currency = self.get_rate_in_profit_currency(
            paid_with_asset,
            timestamp,
        )
        with_sold_asset_gain = sold_asset_rate_in_profit_currency * sold_amount

        # Consider as value of the sell what would give the least profit
        if (bought_asset_rate_in_profit_currency == -1
                or with_sold_asset_gain < with_bought_asset_gain):
            receiving_asset = self.profit_currency
            receiving_amount = with_sold_asset_gain
            rate_in_profit_currency = sold_asset_rate_in_profit_currency
            gain_in_profit_currency = with_sold_asset_gain

        self.add_sell(
            location=location,
            selling_asset=paid_with_asset,
            selling_amount=sold_amount,
            receiving_asset=receiving_asset,
            receiving_amount=receiving_amount,
            rate_in_profit_currency=rate_in_profit_currency,
            gain_in_profit_currency=gain_in_profit_currency,
            total_fee_in_profit_currency=fee_in_profit_currency,
            timestamp=timestamp,
            is_virtual=True,
        )
Example #21
0
    def query_historical_price(from_asset: Asset, to_asset: Asset,
                               timestamp: Timestamp) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        Args:
            from_asset: The ticker symbol of the asset for which we want to know
                        the price.
            to_asset: The ticker symbol of the asset against which we want to
                      know the price.
            timestamp: The timestamp at which to query the price

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from the external service.
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        log.debug(
            'Querying historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
        )

        if from_asset == to_asset:
            return Price(FVal('1'))

        if from_asset.is_fiat() and to_asset.is_fiat():
            # if we are querying historical forex data then try something other than cryptocompare
            price = Inquirer().query_historical_fiat_exchange_rates(
                from_fiat_currency=from_asset,
                to_fiat_currency=to_asset,
                timestamp=timestamp,
            )
            if price is not None:
                return price
            # else cryptocompare also has historical fiat to fiat data

        instance = PriceHistorian()
        price = None
        if Inquirer()._cryptocompare.rate_limited_in_last() is False:
            try:
                price = instance._cryptocompare.query_historical_price(
                    from_asset=from_asset,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
            except (PriceQueryUnsupportedAsset, NoPriceForGivenTimestamp,
                    RemoteError):
                # then use coingecko
                pass

        if price and price != Price(ZERO):
            return price

        try:
            price = instance._coingecko.historical_price(
                from_asset=from_asset,
                to_asset=to_asset,
                time=timestamp,
            )
            if price != Price(ZERO):
                return price
        except RemoteError:
            pass

        # nothing found in any price oracle
        raise NoPriceForGivenTimestamp(
            from_asset=from_asset,
            to_asset=to_asset,
            date=timestamp_to_date(timestamp, formatstr='%d/%m/%Y, %H:%M:%S'),
        )
Example #22
0
    def query_historical_price(
        self,
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
        historical_data_start: Timestamp,
    ) -> Price:
        if from_asset in KNOWN_TO_MISS_FROM_CRYPTOCOMPARE:
            raise PriceQueryUnknownFromAsset(from_asset)

        data = self.get_historical_data(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            historical_data_start=historical_data_start,
        )

        # all data are sorted and timestamps are always increasing by 1 hour
        # find the closest entry to the provided timestamp
        if timestamp >= data[0].time:
            index = convert_to_int((timestamp - data[0].time) / 3600,
                                   accept_only_exact=False)
            # print("timestamp: {} index: {} data_length: {}".format(timestamp, index, len(data)))
            diff = abs(data[index].time - timestamp)
            if index + 1 <= len(data) - 1:
                diff_p1 = abs(data[index + 1].time - timestamp)
                if diff_p1 < diff:
                    index = index + 1

            if data[index].high is None or data[index].low is None:
                # If we get some None in the hourly set price to 0 so that we check alternatives
                price = Price(ZERO)
            else:
                price = (data[index].high + data[index].low) / 2
        else:
            # no price found in the historical data from/to asset, try alternatives
            price = Price(ZERO)

        if price == 0:
            if from_asset != 'BTC' and to_asset != 'BTC':
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp}. Comparing with BTC...",
                )
                # Just get the BTC price
                asset_btc_price = PriceHistorian().query_historical_price(
                    from_asset=from_asset,
                    to_asset=A_BTC,
                    timestamp=timestamp,
                )
                btc_to_asset_price = PriceHistorian().query_historical_price(
                    from_asset=A_BTC,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
                price = asset_btc_price * btc_to_asset_price
            else:
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp} through cryptocompare."
                    f" Attempting to get daily price...", )
                price = self.query_endpoint_pricehistorical(
                    from_asset, to_asset, timestamp)

        comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD)
                                     or (from_asset.is_fiat()
                                         and from_asset != A_USD))
        if comparison_to_nonusd_fiat:
            price = self._adjust_to_cryptocompare_price_incosistencies(
                price=price,
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
            )

        if price == 0:
            raise NoPriceForGivenTimestamp(
                from_asset,
                to_asset,
                timestamp_to_date(timestamp, formatstr='%d/%m/%Y, %H:%M:%S'),
            )

        log.debug(
            'Got historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            price=price,
        )

        return price
Example #23
0
    def find_usd_price(
        asset: Asset,
        ignore_cache: bool = False,
    ) -> Price:
        """Returns the current USD price of the asset

        Returns Price(ZERO) if all options have been exhausted and errors are logged in the logs
        """
        if asset == A_USD:
            return Price(FVal(1))

        instance = Inquirer()
        cache_key = (asset, A_USD)
        if ignore_cache is False:
            cache = instance.get_cached_current_price_entry(
                cache_key=cache_key)
            if cache is not None:
                return cache.price

        if asset.is_fiat():
            try:
                return instance._query_fiat_pair(base=asset, quote=A_USD)
            except RemoteError:
                pass  # continue, a price can be found by one of the oracles (CC for example)

        # Try and check if it is an ethereum token with specified protocol or underlying tokens
        is_known_protocol = False
        underlying_tokens = None
        try:
            token = EthereumToken.from_asset(asset)
            if token is not None:
                if token.protocol is not None:
                    is_known_protocol = token.protocol in KnownProtocolsAssets
                underlying_tokens = GlobalDBHandler(
                ).get_ethereum_token(  # type: ignore
                    token.ethereum_address, ).underlying_tokens
        except UnknownAsset:
            pass

        # Check if it is a special token
        if asset in instance.special_tokens:
            ethereum = instance._ethereum
            assert ethereum, 'Inquirer should never be called before the injection of ethereum'
            assert token, 'all assets in special tokens are already ethereum tokens'
            underlying_asset_price = get_underlying_asset_price(token)
            usd_price = handle_defi_price_query(
                ethereum=ethereum,
                token=token,
                underlying_asset_price=underlying_asset_price,
            )
            if usd_price is None:
                price = Price(ZERO)
            else:
                price = Price(usd_price)

            Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                price=price, time=ts_now())  # noqa: E501
            return price

        if is_known_protocol is True or underlying_tokens is not None:
            assert token is not None
            result = get_underlying_asset_price(token)
            usd_price = Price(ZERO) if result is None else Price(result)
            Inquirer._cached_current_price[cache_key] = CachedPriceEntry(
                price=usd_price,
                time=ts_now(),
            )
            return usd_price

        return instance._query_oracle_instances(from_asset=asset,
                                                to_asset=A_USD)
Example #24
0
    def query_historical_price(
        self,
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
        historical_data_start: Timestamp,
    ) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is known to miss from cryptocompare
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the cryptocompare server
        or with reading the response returned by the server
        """

        try:
            data = self.get_historical_data(
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
                historical_data_start=historical_data_start,
            )
        except UnsupportedAsset as e:
            raise PriceQueryUnsupportedAsset(e.asset_name)

        price = Price(ZERO)
        # all data are sorted and timestamps are always increasing by 1 hour
        # find the closest entry to the provided timestamp
        if timestamp >= data[0].time:
            index_in_bounds = True
            # convert_to_int can't raise here due to its input
            index = convert_to_int((timestamp - data[0].time) / 3600,
                                   accept_only_exact=False)
            if index > len(data) - 1:  # index out of bounds
                # Try to see if index - 1 is there and if yes take it
                if index > len(data):
                    index = index - 1
                else:  # give up. This happened: https://github.com/rotki/rotki/issues/1534
                    log.error(
                        f'Expected data index in cryptocompare historical hour price '
                        f'not found. Queried price of: {from_asset.identifier} in '
                        f'{to_asset.identifier} at {timestamp}. Data '
                        f'index: {index}. Length of returned data: {len(data)}. '
                        f'https://github.com/rotki/rotki/issues/1534. Attempting other methods...',
                    )
                    index_in_bounds = False

            if index_in_bounds:
                diff = abs(data[index].time - timestamp)
                if index + 1 <= len(data) - 1:
                    diff_p1 = abs(data[index + 1].time - timestamp)
                    if diff_p1 < diff:
                        index = index + 1

                if data[index].high is not None and data[index].low is not None:
                    price = Price((data[index].high + data[index].low) / 2)

        else:
            # no price found in the historical data from/to asset, try alternatives
            price = Price(ZERO)

        if price == 0:
            if from_asset != 'BTC' and to_asset != 'BTC':
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp}. Comparing with BTC...",
                )
                # Just get the BTC price
                asset_btc_price = PriceHistorian().query_historical_price(
                    from_asset=from_asset,
                    to_asset=A_BTC,
                    timestamp=timestamp,
                )
                btc_to_asset_price = PriceHistorian().query_historical_price(
                    from_asset=A_BTC,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
                price = Price(asset_btc_price * btc_to_asset_price)
            else:
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp} through cryptocompare."
                    f" Attempting to get daily price...", )
                price = self.query_endpoint_pricehistorical(
                    from_asset, to_asset, timestamp)

        comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD)
                                     or (from_asset.is_fiat()
                                         and from_asset != A_USD))
        if comparison_to_nonusd_fiat:
            price = self._adjust_to_cryptocompare_price_incosistencies(
                price=price,
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
            )

        if price == 0:
            raise NoPriceForGivenTimestamp(
                from_asset=from_asset,
                to_asset=to_asset,
                date=timestamp_to_date(timestamp,
                                       formatstr='%d/%m/%Y, %H:%M:%S'),
            )

        log.debug(
            'Got historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            price=price,
        )

        return price
Example #25
0
    def add_sell(
        self,
        location: Location,
        selling_asset: Asset,
        selling_amount: FVal,
        receiving_asset: Optional[Asset],
        receiving_amount: Optional[FVal],
        gain_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        rate_in_profit_currency: FVal,
        timestamp: Timestamp,
        loan_settlement: bool = False,
        is_virtual: bool = False,
    ) -> None:
        """Account for the given sell action

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        skip_trade = (not self.include_crypto2crypto
                      and not selling_asset.is_fiat() and receiving_asset
                      and not receiving_asset.is_fiat())
        if skip_trade:
            return

        if selling_amount == ZERO:
            logger.error(
                f'Skipping sell trade of {selling_asset.identifier} for '
                f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp}'
                f' since the selling amount is 0', )
            return

        if selling_asset.is_fiat():
            # Should be handled by a virtual buy
            logger.debug(
                f'Skipping sell trade of {selling_asset.identifier} for '
                f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp} '
                f'since selling of FIAT of something will just be treated as a buy.',
            )
            return

        logger.debug(
            f'Processing sell trade of {selling_asset.identifier} for '
            f'{receiving_asset.identifier if receiving_asset else "nothing"} at {timestamp}',
        )

        self.cost_basis.spend_asset(
            location=location,
            timestamp=timestamp,
            asset=selling_asset,
            amount=selling_amount,
            rate=rate_in_profit_currency,
            fee_in_profit_currency=total_fee_in_profit_currency,
            gain_in_profit_currency=gain_in_profit_currency,
        )
        self.handle_prefork_asset_sells(selling_asset, selling_amount,
                                        timestamp)

        # now search the acquisitions for `paid_with_asset` and calculate profit/loss
        cost_basis_info = self.cost_basis.calculate_spend_cost_basis(
            spending_amount=selling_amount,
            spending_asset=selling_asset,
            timestamp=timestamp,
        )
        general_profit_loss = ZERO
        taxable_profit_loss = ZERO

        # If we don't include crypto2crypto and we sell for crypto, stop here
        if receiving_asset and not receiving_asset.is_fiat(
        ) and not self.include_crypto2crypto:
            return

        # calculate profit/loss
        if not loan_settlement or (loan_settlement
                                   and self.count_profit_for_settlements):
            taxable_gain = taxable_gain_for_sell(
                taxable_amount=cost_basis_info.taxable_amount,
                rate_in_profit_currency=rate_in_profit_currency,
                total_fee_in_profit_currency=total_fee_in_profit_currency,
                selling_amount=selling_amount,
            )

            total_bought_cost_in_profit_currency = (
                cost_basis_info.taxfree_bought_cost +
                cost_basis_info.taxable_bought_cost +
                total_fee_in_profit_currency)
            general_profit_loss = gain_in_profit_currency - total_bought_cost_in_profit_currency
            taxable_profit_loss = taxable_gain - cost_basis_info.taxable_bought_cost

        # should never happen, should be stopped at the main loop
        assert timestamp <= self.query_end_ts, (
            'Trade time > query_end_ts found in adding to sell event')

        # count profit/losses if we are inside the query period
        if timestamp >= self.query_start_ts:
            if loan_settlement:
                # If it's a loan settlement we are charged both the fee and the gain
                settlement_loss = gain_in_profit_currency + total_fee_in_profit_currency
                expected = rate_in_profit_currency * selling_amount + total_fee_in_profit_currency
                msg = (
                    f'Expected settlement loss mismatch. rate_in_profit_currency'
                    f' ({rate_in_profit_currency}) * selling_amount'
                    f' ({selling_amount}) + total_fee_in_profit_currency'
                    f' ({total_fee_in_profit_currency}) != settlement_loss '
                    f'({settlement_loss})')
                assert expected == settlement_loss, msg
                self.settlement_losses += settlement_loss
                log.debug(
                    'Loan Settlement Loss',
                    settlement_loss=settlement_loss,
                    profit_currency=self.profit_currency,
                )
            else:
                log.debug(
                    "After Sell Profit/Loss",
                    taxable_profit_loss=taxable_profit_loss,
                    general_profit_loss=general_profit_loss,
                    profit_currency=self.profit_currency,
                )

            self.general_trade_profit_loss += general_profit_loss
            self.taxable_trade_profit_loss += taxable_profit_loss

            if loan_settlement:
                self.csv_exporter.add_loan_settlement(
                    location=location,
                    asset=selling_asset,
                    amount=selling_amount,
                    rate_in_profit_currency=rate_in_profit_currency,
                    total_fee_in_profit_currency=total_fee_in_profit_currency,
                    timestamp=timestamp,
                    cost_basis_info=cost_basis_info,
                )
            else:
                assert receiving_asset, 'Here receiving asset should have a value'
                self.csv_exporter.add_sell(
                    location=location,
                    selling_asset=selling_asset,
                    rate_in_profit_currency=rate_in_profit_currency,
                    total_fee_in_profit_currency=total_fee_in_profit_currency,
                    gain_in_profit_currency=gain_in_profit_currency,
                    selling_amount=selling_amount,
                    receiving_asset=receiving_asset,
                    receiving_amount=receiving_amount,
                    receiving_asset_rate_in_profit_currency=self.
                    get_rate_in_profit_currency(
                        receiving_asset,
                        timestamp,
                    ),
                    taxable_amount=cost_basis_info.taxable_amount,
                    taxable_bought_cost=cost_basis_info.taxable_bought_cost,
                    timestamp=timestamp,
                    cost_basis_info=cost_basis_info,
                    is_virtual=is_virtual,
                    total_bought_cost=total_bought_cost_in_profit_currency,
                )
Example #26
0
    def query_historical_fiat_exchange_rates(
        from_fiat_currency: Asset,
        to_fiat_currency: Asset,
        timestamp: Timestamp,
    ) -> Optional[Price]:
        assert from_fiat_currency.is_fiat(
        ), 'fiat currency should have been provided'
        assert to_fiat_currency.is_fiat(
        ), 'fiat currency should have been provided'
        date = timestamp_to_date(timestamp, formatstr='%Y-%m-%d')
        instance = Inquirer()
        rate = instance._get_cached_forex_data(date, from_fiat_currency,
                                               to_fiat_currency)
        if rate:
            return rate

        log.debug(
            'Querying exchangeratesapi',
            from_fiat_currency=from_fiat_currency.identifier,
            to_fiat_currency=to_fiat_currency.identifier,
            timestamp=timestamp,
        )

        query_str = (f'https://api.exchangeratesapi.io/{date}?'
                     f'base={from_fiat_currency.identifier}')
        resp = retry_calls(
            times=5,
            location='query_exchangeratesapi',
            handle_429=False,
            backoff_in_seconds=0,
            method_name='requests.get',
            function=requests.get,
            # function's arguments
            url=query_str,
        )

        if resp.status_code != 200:
            return None

        try:
            result = rlk_jsonloads_dict(resp.text)
        except JSONDecodeError:
            return None

        if 'rates' not in result or to_fiat_currency.identifier not in result[
                'rates']:
            return None

        if date not in instance._cached_forex_data:
            instance._cached_forex_data[date] = {}

        if from_fiat_currency not in instance._cached_forex_data[date]:
            instance._cached_forex_data[date][from_fiat_currency] = {}

        for key, value in result['rates'].items():
            instance._cached_forex_data[date][from_fiat_currency][key] = FVal(
                value)

        rate = Price(FVal(result['rates'][to_fiat_currency.identifier]))
        log.debug('Exchangeratesapi query succesful', rate=rate)
        return rate
Example #27
0
    def add_buy(
        self,
        location: Location,
        bought_asset: Asset,
        bought_amount: FVal,
        paid_with_asset: Asset,
        trade_rate: FVal,
        fee_in_profit_currency: Fee,
        timestamp: Timestamp,
        is_virtual: bool = False,
        is_from_prefork_virtual_buy: bool = False,
    ) -> None:
        """
        Account for the given buy

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from all price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        skip_trade = (not self.include_crypto2crypto
                      and not bought_asset.is_fiat()
                      and not paid_with_asset.is_fiat())
        if skip_trade:
            return

        logger.debug(
            f'Processing buy trade of {bought_asset.identifier} with '
            f'{paid_with_asset.identifier} at {timestamp}', )

        paid_with_asset_rate = self.get_rate_in_profit_currency(
            paid_with_asset, timestamp)
        buy_rate = paid_with_asset_rate * trade_rate

        self.handle_prefork_asset_buys(
            location=location,
            bought_asset=bought_asset,
            bought_amount=bought_amount,
            paid_with_asset=paid_with_asset,
            trade_rate=trade_rate,
            fee_in_profit_currency=fee_in_profit_currency,
            timestamp=timestamp,
            is_from_prefork_virtual_buy=is_from_prefork_virtual_buy,
        )

        gross_cost = bought_amount * buy_rate
        cost_in_profit_currency = gross_cost + fee_in_profit_currency

        self.cost_basis.obtain_asset(
            location=location,
            timestamp=timestamp,
            description='trade',
            asset=bought_asset,
            amount=bought_amount,
            rate=buy_rate,
            fee_in_profit_currency=fee_in_profit_currency,
        )
        if timestamp >= self.query_start_ts:
            self.csv_exporter.add_buy(
                location=location,
                bought_asset=bought_asset,
                rate_in_profit_currency=buy_rate,
                fee_cost=fee_in_profit_currency,
                amount=bought_amount,
                cost_in_profit_currency=cost_in_profit_currency,
                paid_with_asset=paid_with_asset,
                paid_with_asset_rate=paid_with_asset_rate,
                paid_with_asset_amount=trade_rate * bought_amount,
                timestamp=timestamp,
                is_virtual=is_virtual,
            )
Example #28
0
    def query_historical_price(
        self,
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
        historical_data_start: Timestamp,
    ) -> Price:
        """
        Query the historical price on `timestamp` for `from_asset` in `to_asset`.
        So how much `to_asset` does 1 unit of `from_asset` cost.

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is known to miss from cryptocompare
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the cryptocompare server
        or with reading the response returned by the server
        """

        try:
            data = self.get_historical_data(
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
                historical_data_start=historical_data_start,
            )
        except UnsupportedAsset as e:
            raise PriceQueryUnsupportedAsset(e.asset_name)

        # all data are sorted and timestamps are always increasing by 1 hour
        # find the closest entry to the provided timestamp
        if timestamp >= data[0].time:
            # convert_to_int can't raise here due to its input
            index = convert_to_int((timestamp - data[0].time) / 3600,
                                   accept_only_exact=False)
            # print("timestamp: {} index: {} data_length: {}".format(timestamp, index, len(data)))
            diff = abs(data[index].time - timestamp)
            if index + 1 <= len(data) - 1:
                diff_p1 = abs(data[index + 1].time - timestamp)
                if diff_p1 < diff:
                    index = index + 1

            if data[index].high is None or data[index].low is None:
                # If we get some None in the hourly set price to 0 so that we check alternatives
                price = Price(ZERO)
            else:
                price = Price((data[index].high + data[index].low) / 2)
        else:
            # no price found in the historical data from/to asset, try alternatives
            price = Price(ZERO)

        if price == 0:
            if from_asset != 'BTC' and to_asset != 'BTC':
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp}. Comparing with BTC...",
                )
                # Just get the BTC price
                asset_btc_price = PriceHistorian().query_historical_price(
                    from_asset=from_asset,
                    to_asset=A_BTC,
                    timestamp=timestamp,
                )
                btc_to_asset_price = PriceHistorian().query_historical_price(
                    from_asset=A_BTC,
                    to_asset=to_asset,
                    timestamp=timestamp,
                )
                price = Price(asset_btc_price * btc_to_asset_price)
            else:
                log.debug(
                    f"Couldn't find historical price from {from_asset} to "
                    f"{to_asset} at timestamp {timestamp} through cryptocompare."
                    f" Attempting to get daily price...", )
                price = self.query_endpoint_pricehistorical(
                    from_asset, to_asset, timestamp)

        comparison_to_nonusd_fiat = ((to_asset.is_fiat() and to_asset != A_USD)
                                     or (from_asset.is_fiat()
                                         and from_asset != A_USD))
        if comparison_to_nonusd_fiat:
            price = self._adjust_to_cryptocompare_price_incosistencies(
                price=price,
                from_asset=from_asset,
                to_asset=to_asset,
                timestamp=timestamp,
            )

        if price == 0:
            raise NoPriceForGivenTimestamp(
                from_asset=from_asset,
                to_asset=to_asset,
                date=timestamp_to_date(timestamp,
                                       formatstr='%d/%m/%Y, %H:%M:%S'),
            )

        log.debug(
            'Got historical price',
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            price=price,
        )

        return price
Example #29
0
    def add_sell_and_corresponding_buy(
        self,
        location: Location,
        selling_asset: Asset,
        selling_amount: FVal,
        receiving_asset: Asset,
        receiving_amount: FVal,
        gain_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        trade_rate: FVal,
        rate_in_profit_currency: FVal,
        timestamp: Timestamp,
    ) -> None:
        """
        Account for the given sell and the corresponding buy if it's a crypto to crypto

        Args:
            selling_asset (str): The ticker representation of the asset we sell.
            selling_amount (FVal): The amount of `selling_asset` for sale.
            receiving_asset (str): The ticker representation of the asset we receive
                                   in exchange for `selling_asset`.
            receiving_amount (FVal): The amount of `receiving_asset` we receive.
            gain_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent
                                            we receive after doing this trade. Fees are not counted
                                            in this.
            total_fee_in_profit_currency (FVal): This is the amount of `profit_currency` equivalent
                                                 we pay in fees after doing this trade.
            trade_rate (FVal): How much does 1 unit of `receiving_asset` cost in `selling_asset`
            rate_in_profit_currency (FVal): The equivalent of `trade_rate` in `profit_currency`
            timestamp (int): The timestamp for the trade

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        self.add_sell(
            location=location,
            selling_asset=selling_asset,
            selling_amount=selling_amount,
            receiving_asset=receiving_asset,
            receiving_amount=receiving_amount,
            gain_in_profit_currency=gain_in_profit_currency,
            total_fee_in_profit_currency=total_fee_in_profit_currency,
            rate_in_profit_currency=rate_in_profit_currency,
            timestamp=timestamp,
            is_virtual=False,
        )

        if receiving_asset.is_fiat() or not self.include_crypto2crypto:
            return

        log.debug(
            f'Selling {selling_asset} for {receiving_asset} also introduces a virtual buy event',
        )
        # else then you are also buying some other asset through your sell
        assert trade_rate != 0, 'Trade rate should not be zero at this point'
        self.add_buy(
            location=location,
            bought_asset=receiving_asset,
            bought_amount=receiving_amount,
            paid_with_asset=selling_asset,
            trade_rate=1 / trade_rate,
            fee_in_profit_currency=total_fee_in_profit_currency,
            timestamp=timestamp,
            is_virtual=True,
        )
Example #30
0
    def add_sell(
        self,
        location: Location,
        selling_asset: Asset,
        selling_amount: FVal,
        receiving_asset: Optional[Asset],
        receiving_amount: Optional[FVal],
        gain_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        trade_rate: FVal,
        rate_in_profit_currency: FVal,
        timestamp: Timestamp,
        loan_settlement: bool = False,
        is_virtual: bool = False,
    ) -> None:
        """Account for the given sell action

        May raise:
        - PriceQueryUnsupportedAsset if from/to asset is missing from price oracles
        - NoPriceForGivenTimestamp if we can't find a price for the asset in the given
        timestamp from cryptocompare
        - RemoteError if there is a problem reaching the price oracle server
        or with reading the response returned by the server
        """
        skip_trade = (not self.include_crypto2crypto
                      and not selling_asset.is_fiat() and receiving_asset
                      and not receiving_asset.is_fiat())
        if skip_trade:
            return

        if selling_asset not in self.events:
            self.events[selling_asset] = Events([], [])

        self.events[selling_asset].sells.append(
            SellEvent(
                amount=selling_amount,
                timestamp=timestamp,
                rate=rate_in_profit_currency,
                fee_rate=total_fee_in_profit_currency / selling_amount,
                gain=gain_in_profit_currency,
            ), )

        self.handle_prefork_asset_sells(selling_asset, selling_amount,
                                        timestamp)

        if loan_settlement:
            log.debug(
                'Loan Settlement Selling Event',
                sensitive_log=True,
                selling_amount=selling_amount,
                selling_asset=selling_asset,
                gain_in_profit_currency=gain_in_profit_currency,
                profit_currency=self.profit_currency,
                timestamp=timestamp,
            )
        else:
            log.debug(
                'Selling Event',
                sensitive_log=True,
                selling_amount=selling_amount,
                selling_asset=selling_asset,
                receiving_amount=receiving_amount,
                receiving_asset=receiving_asset,
                rate=trade_rate,
                rate_in_profit_currency=rate_in_profit_currency,
                profit_currency=self.profit_currency,
                gain_in_profit_currency=gain_in_profit_currency,
                fee_in_profit_currency=total_fee_in_profit_currency,
                timestamp=timestamp,
            )

        # now search the buys for `paid_with_asset` and calculate profit/loss
        (
            taxable_amount,
            taxable_bought_cost,
            taxfree_bought_cost,
        ) = self.search_buys_calculate_profit(
            selling_amount,
            selling_asset,
            timestamp,
        )
        general_profit_loss = ZERO
        taxable_profit_loss = ZERO

        # If we don't include crypto2crypto and we sell for crypto, stop here
        if receiving_asset and not receiving_asset.is_fiat(
        ) and not self.include_crypto2crypto:
            return

        # calculate profit/loss
        if not loan_settlement or (loan_settlement
                                   and self.count_profit_for_settlements):
            taxable_gain = taxable_gain_for_sell(
                taxable_amount=taxable_amount,
                rate_in_profit_currency=rate_in_profit_currency,
                total_fee_in_profit_currency=total_fee_in_profit_currency,
                selling_amount=selling_amount,
            )

            general_profit_loss = gain_in_profit_currency - (
                taxfree_bought_cost + taxable_bought_cost +
                total_fee_in_profit_currency)
            taxable_profit_loss = taxable_gain - taxable_bought_cost

        # should never happen, should be stopped at the main loop
        assert timestamp <= self.query_end_ts, (
            "Trade time > query_end_ts found in adding to sell event")

        # count profit/losses if we are inside the query period
        if timestamp >= self.query_start_ts:
            if loan_settlement:
                # If it's a loan settlement we are charged both the fee and the gain
                settlement_loss = gain_in_profit_currency + total_fee_in_profit_currency
                expected = rate_in_profit_currency * selling_amount + total_fee_in_profit_currency
                msg = (
                    f'Expected settlement loss mismatch. rate_in_profit_currency'
                    f' ({rate_in_profit_currency}) * selling_amount'
                    f' ({selling_amount}) + total_fee_in_profit_currency'
                    f' ({total_fee_in_profit_currency}) != settlement_loss '
                    f'({settlement_loss})')
                assert expected == settlement_loss, msg
                self.settlement_losses += settlement_loss
                log.debug(
                    'Loan Settlement Loss',
                    sensitive_log=True,
                    settlement_loss=settlement_loss,
                    profit_currency=self.profit_currency,
                )
            else:
                log.debug(
                    "After Sell Profit/Loss",
                    sensitive_log=True,
                    taxable_profit_loss=taxable_profit_loss,
                    general_profit_loss=general_profit_loss,
                    profit_currency=self.profit_currency,
                )

            self.general_trade_profit_loss += general_profit_loss
            self.taxable_trade_profit_loss += taxable_profit_loss

            if loan_settlement:
                self.csv_exporter.add_loan_settlement(
                    location=location,
                    asset=selling_asset,
                    amount=selling_amount,
                    rate_in_profit_currency=rate_in_profit_currency,
                    total_fee_in_profit_currency=total_fee_in_profit_currency,
                    timestamp=timestamp,
                )
            else:
                assert receiving_asset, 'Here receiving asset should have a value'
                self.csv_exporter.add_sell(
                    location=location,
                    selling_asset=selling_asset,
                    rate_in_profit_currency=rate_in_profit_currency,
                    total_fee_in_profit_currency=total_fee_in_profit_currency,
                    gain_in_profit_currency=gain_in_profit_currency,
                    selling_amount=selling_amount,
                    receiving_asset=receiving_asset,
                    receiving_amount=receiving_amount,
                    receiving_asset_rate_in_profit_currency=self.
                    get_rate_in_profit_currency(
                        receiving_asset,
                        timestamp,
                    ),
                    taxable_amount=taxable_amount,
                    taxable_bought_cost=taxable_bought_cost,
                    timestamp=timestamp,
                    is_virtual=is_virtual,
                )