Beispiel #1
0
    def add_loan_profit(
        self,
        gained_asset: Asset,
        gained_amount: FVal,
        gain_in_profit_currency: FVal,
        lent_amount: FVal,
        open_time: Timestamp,
        close_time: Timestamp,
    ) -> None:
        if not self.create_csv:
            return

        self.loan_profits_csv.append({
            'open_time':
            timestamp_to_date(open_time, formatstr='%d/%m/%Y %H:%M:%S'),
            'close_time':
            timestamp_to_date(close_time, formatstr='%d/%m/%Y %H:%M:%S'),
            'gained_asset':
            gained_asset.identifier,
            'gained_amount':
            gained_amount,
            'lent_amount':
            lent_amount,
            f'profit_in_{self.profit_currency.identifier}':
            gain_in_profit_currency,
        })
        self.add_to_allevents(
            event_type=EV_INTEREST_PAYMENT,
            paid_in_profit_currency=FVal(0),
            paid_asset=S_EMPTYSTR,
            paid_in_asset=FVal(0),
            received_asset=gained_asset,
            received_in_asset=gained_amount,
            taxable_received_in_profit_currency=gain_in_profit_currency,
            timestamp=close_time,
        )
Beispiel #2
0
 def __init__(self, from_asset: Asset, to_asset: Asset,
              time: Timestamp) -> None:
     self.from_asset = from_asset
     self.to_asset = to_asset
     self.time = time
     super().__init__(
         'Unable to query a historical price for "{}" to "{}" at {}'.format(
             from_asset.identifier,
             to_asset.identifier,
             timestamp_to_date(
                 ts=time,
                 formatstr='%d/%m/%Y, %H:%M:%S',
                 treat_as_local=True,
             ),
         ), )
Beispiel #3
0
    def query_fiat_pair(base: Asset, quote: Asset) -> Price:
        if base == quote:
            return Price(FVal('1'))

        instance = Inquirer()
        now = ts_now()
        date = timestamp_to_date(ts_now(), formatstr='%Y-%m-%d')
        price = instance._get_cached_forex_data(date, base, quote)
        if price:
            return price

        price = _query_exchanges_rateapi(base, quote)
        # TODO: Find another backup API for fiat exchange rates

        if not price:
            # Search the cache for any price in the last month
            for i in range(1, 31):
                now = Timestamp(now - Timestamp(86401))
                date = timestamp_to_date(now, formatstr='%Y-%m-%d')
                price = instance._get_cached_forex_data(date, base, quote)
                if price:
                    log.debug(
                        f'Could not query online apis for a fiat price. '
                        f'Used cached value from {i} days ago.',
                        base_currency=base.identifier,
                        quote_currency=quote.identifier,
                        price=price,
                    )
                    return price

            raise ValueError(
                'Could not find a "{}" price for "{}"'.format(
                    base.identifier, quote.identifier), )

        instance._save_forex_rate(date, base, quote, price)
        return price
Beispiel #4
0
    def add_defi_event(self, event: DefiEvent,
                       profit_loss_in_profit_currency: FVal) -> None:
        if not self.create_csv:
            return

        self.defi_events_csv.append({
            'time':
            timestamp_to_date(event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'type':
            str(event.event_type),
            'asset':
            str(event.asset),
            'amount':
            str(event.amount),
            f'profit_loss_in_{self.profit_currency.identifier}':
            profit_loss_in_profit_currency,
        })

        paid_asset: Union[EmptyStr, Asset]
        received_asset: Union[EmptyStr, Asset]
        if event.event_type == DefiEventType.DSR_LOAN_GAIN:
            paid_in_profit_currency = ZERO
            paid_in_asset = ZERO
            paid_asset = S_EMPTYSTR
            received_asset = event.asset
            received_in_asset = event.amount
            received_in_profit_currency = profit_loss_in_profit_currency
        elif event.event_type == DefiEventType.MAKERDAO_VAULT_LOSS:
            paid_in_profit_currency = profit_loss_in_profit_currency
            paid_in_asset = event.amount
            paid_asset = event.asset
            received_asset = S_EMPTYSTR
            received_in_asset = ZERO
            received_in_profit_currency = ZERO
        else:
            raise NotImplementedError(
                'Not implemented Defi event encountered at csv export')

        self.add_to_allevents(
            event_type=EV_DEFI,
            paid_in_profit_currency=paid_in_profit_currency,
            paid_asset=paid_asset,
            paid_in_asset=paid_in_asset,
            received_asset=received_asset,
            received_in_asset=received_in_asset,
            taxable_received_in_profit_currency=received_in_profit_currency,
            timestamp=event.timestamp,
        )
Beispiel #5
0
    def add_defi_event(self, event: DefiEvent,
                       profit_loss_in_profit_currency: FVal) -> None:
        if not self.create_csv:
            return

        self.defi_events_csv.append({
            'time':
            timestamp_to_date(event.timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'type':
            str(event.event_type),
            'asset':
            str(event.asset),
            'amount':
            str(event.amount),
            f'profit_loss_in_{self.profit_currency.identifier}':
            profit_loss_in_profit_currency,
        })

        paid_asset: Union[EmptyStr, Asset]
        received_asset: Union[EmptyStr, Asset]
        if event.is_profitable():
            paid_in_profit_currency = ZERO
            paid_in_asset = ZERO
            paid_asset = S_EMPTYSTR
            received_asset = event.asset
            received_in_asset = event.amount
            received_in_profit_currency = profit_loss_in_profit_currency
        else:
            paid_in_profit_currency = profit_loss_in_profit_currency
            paid_in_asset = event.amount
            paid_asset = event.asset
            received_asset = S_EMPTYSTR
            received_in_asset = ZERO
            received_in_profit_currency = ZERO

        self.add_to_allevents(
            event_type=EV_DEFI,
            location=Location.BLOCKCHAIN,
            paid_in_profit_currency=paid_in_profit_currency,
            paid_asset=paid_asset,
            paid_in_asset=paid_in_asset,
            received_asset=received_asset,
            received_in_asset=received_in_asset,
            taxable_received_in_profit_currency=received_in_profit_currency,
            timestamp=event.timestamp,
        )
Beispiel #6
0
    def historical_price(self, from_asset: Asset, to_asset: Asset,
                         time: Timestamp) -> Price:
        vs_currency = Coingecko.check_vs_currencies(
            from_asset=from_asset,
            to_asset=to_asset,
            location='historical price',
        )
        if not vs_currency:
            return Price(ZERO)

        if from_asset.coingecko is None:
            log.warning(
                f'Tried to query coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But from_asset is not supported in coingecko',
            )
            return Price(ZERO)

        date = timestamp_to_date(time, formatstr='%d-%m-%Y')
        cached_price = self._get_cached_price(from_asset=from_asset,
                                              to_asset=to_asset,
                                              date=date)
        if cached_price is not None:
            return cached_price

        result = self._query(
            module='coins',
            subpath=f'{from_asset.coingecko}/history',
            options={
                'date': date,
                'localization': False,
            },
        )

        try:
            price = Price(
                FVal(result['market_data']['current_price'][vs_currency]))
        except KeyError as e:
            log.warning(
                f'Queried coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But got key error for {str(e)} when '
                f'processing the result.', )
            return Price(ZERO)

        self._save_cached_price(from_asset, to_asset, date, price)
        return price
Beispiel #7
0
    def decompress_and_decrypt_db(self, password: str, encrypted_data: B64EncodedString) -> None:
        """Decrypt and decompress the encrypted data we receive from the server

        If successful then replace our local Database

        Can raise UnableToDecryptRemoteData due to decrypt().
        """
        log.info('Decompress and decrypt DB')

        # First make a backup of the DB we are about to replace
        date = timestamp_to_date(ts=ts_now(), formatstr='%Y_%m_%d_%H_%M_%S')
        shutil.copyfile(
            os.path.join(self.data_directory, self.username, 'rotkehlchen.db'),
            os.path.join(self.data_directory, self.username, f'rotkehlchen_db_{date}.backup'),
        )

        decrypted_data = decrypt(password.encode(), encrypted_data)
        decompressed_data = zlib.decompress(decrypted_data)
        self.db.import_unencrypted(decompressed_data, password)
    def add_buy(
            self,
            bought_asset: Asset,
            rate: FVal,
            fee_cost: Fee,
            amount: FVal,
            cost: FVal,
            paid_with_asset: Asset,
            paid_with_asset_rate: FVal,
            timestamp: Timestamp,
            is_virtual: bool,
    ) -> None:
        if not self.create_csv:
            return

        exchange_rate_key = f'exchanged_asset_{self.profit_currency}_exchange_rate'
        self.trades_csv.append({
            'type': 'buy',
            'asset': bought_asset,
            'price_in_{}'.format(self.profit_currency): rate,
            'fee_in_{}'.format(self.profit_currency): fee_cost,
            'gained_or_invested_{}'.format(self.profit_currency): cost,
            'amount': amount,
            'taxable_amount': 'not applicable',  # makes no difference for buying
            'exchanged_for': paid_with_asset,
            exchange_rate_key: paid_with_asset_rate,
            'taxable_bought_cost_in_{}'.format(self.profit_currency): 'not applicable',
            'taxable_gain_in_{}'.format(self.profit_currency): FVal(0),
            'taxable_profit_loss_in_{}'.format(self.profit_currency): FVal(0),
            'time': timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'is_virtual': is_virtual,
        })
        self.add_to_allevents(
            event_type=EV_BUY,
            paid_in_profit_currency=cost,
            paid_asset=self.profit_currency,
            paid_in_asset=cost,
            received_asset=bought_asset,
            received_in_asset=amount,
            taxable_received_in_profit_currency=FVal(0),
            timestamp=timestamp,
            is_virtual=is_virtual,
        )
Beispiel #9
0
 def serialize(
         self,
         export_data: Optional[Tuple[Asset,
                                     Price]] = None) -> Dict[str, str]:
     if export_data:
         return {
             'timestamp':
             timestamp_to_date(self.time, '%Y-%m-%d %H:%M:%S'),
             'location':
             Location.deserialize_from_db(self.location).serialize(),
             f'{export_data[0].symbol.lower()}_value':
             str(FVal(self.usd_value) * export_data[1]),  # noqa: 501
         }
     return {
         'timestamp': str(self.time),
         'location':
         Location.deserialize_from_db(self.location).serialize(),
         'usd_value': self.usd_value,
     }
Beispiel #10
0
    def add_loan_settlement(
        self,
        location: Location,
        asset: Asset,
        amount: FVal,
        rate_in_profit_currency: FVal,
        total_fee_in_profit_currency: FVal,
        timestamp: Timestamp,
    ) -> None:
        if not self.create_csv:
            return

        row = len(self.loan_settlements_csv) + 2
        loss_formula = '=C{}*D{}+E{}'.format(row, row, row)
        self.loan_settlements_csv.append({
            'asset':
            asset.identifier,
            'location':
            str(location),
            'amount':
            amount,
            f'price_in_{self.profit_currency.identifier}':
            rate_in_profit_currency,
            f'fee_in_{self.profit_currency.identifier}':
            total_fee_in_profit_currency,
            f'loss_in_{self.profit_currency.identifier}':
            loss_formula,
            'time':
            timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
        })
        paid_in_profit_currency = amount * rate_in_profit_currency + total_fee_in_profit_currency
        self.add_to_allevents(
            event_type=EV_LOAN_SETTLE,
            location=location,
            paid_in_profit_currency=paid_in_profit_currency,
            paid_asset=asset,
            paid_in_asset=amount,
            received_asset=S_EMPTYSTR,
            received_in_asset=FVal(0),
            taxable_received_in_profit_currency=FVal(0),
            timestamp=timestamp,
        )
Beispiel #11
0
    def add_margin_position(
        self,
        location: Location,
        margin_notes: str,
        gain_loss_asset: Asset,
        gain_loss_amount: FVal,
        gain_loss_in_profit_currency: FVal,
        timestamp: Timestamp,
    ) -> None:
        if not self.create_csv:
            return

        # Note:  We are not getting the fee info in here but they are not needed
        # in the final CSV export.

        self.margin_positions_csv.append({
            'name':
            margin_notes,
            'location':
            str(location),
            'time':
            timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'gain_loss_asset':
            gain_loss_asset.identifier,
            'gain_loss_amount':
            gain_loss_amount,
            f'profit_loss_in_{self.profit_currency.identifier}':
            gain_loss_in_profit_currency,
        })
        self.add_to_allevents(
            event_type=EV_MARGIN_CLOSE,
            location=location,
            paid_in_profit_currency=FVal(0),
            paid_asset=S_EMPTYSTR,
            paid_in_asset=FVal(0),
            received_asset=gain_loss_asset,
            received_in_asset=gain_loss_amount,
            taxable_received_in_profit_currency=gain_loss_in_profit_currency,
            timestamp=timestamp,
        )
Beispiel #12
0
def main() -> None:
    """Goes through the assets template, reads the built-in assets DB and generates
    assets.py with initialization of all constant assets"""
    root_dir = Path(__file__).resolve().parent.parent.parent
    constants_dir = root_dir / 'rotkehlchen' / 'constants'
    template_file = constants_dir / 'assets.py.template'
    date = timestamp_to_date(ts_now())
    generated_text = (
        f'# This python file was generated automatically by\n'
        f'# {__file__} at {date}.\n'
        f'# Do not edit manually!\n'
        f'\n'
    )
    ctx = ContextManager()
    with open(template_file, 'r') as f:
        for line in f:
            line = line.strip('\n\r')
            if 'Asset(\'' in line:
                initial_split = line.split(' = Asset(\'')
                var_name = initial_split[0]
                identifier = initial_split[1].split('\'')[0]
                generated_text += ctx.add_asset_initialization(var_name, identifier)
                continue

            if 'EthereumToken(\'' in line:
                initial_split = line.split(' = EthereumToken(\'')
                var_name = initial_split[0]
                identifier = initial_split[1].split('\'')[0]
                generated_text += ctx.add_ethtoken_initialization(var_name, identifier)
                continue

            # else just copy text
            generated_text += line + '\n'

    assets_file = constants_dir / 'assets.py'
    with open(assets_file, 'w') as f:
        f.write(generated_text)

    print('constants/assets.py generated succesfully!')
Beispiel #13
0
    def decompress_and_decrypt_db(self, password: str, encrypted_data: B64EncodedString) -> None:
        """Decrypt and decompress the encrypted data we receive from the server

        If successful then replace our local Database

        May Raise:
        - UnableToDecryptRemoteData due to decrypt()
        - DBUpgradeError if the rotki DB version is newer than the software or
        there is a DB upgrade and there is an error.
        - SystemPermissionError if the DB file permissions are not correct
        """
        log.info('Decompress and decrypt DB')

        # First make a backup of the DB we are about to replace
        date = timestamp_to_date(ts=ts_now(), formatstr='%Y_%m_%d_%H_%M_%S')
        shutil.copyfile(
            self.data_directory / self.username / 'rotkehlchen.db',
            self.data_directory / self.username / f'rotkehlchen_db_{date}.backup',
        )

        decrypted_data = decrypt(password.encode(), encrypted_data)
        decompressed_data = zlib.decompress(decrypted_data)
        self.db.import_unencrypted(decompressed_data, password)
Beispiel #14
0
    def add_asset_movement(
        self,
        exchange: Location,
        category: AssetMovementCategory,
        asset: Asset,
        fee: Fee,
        rate: FVal,
        timestamp: Timestamp,
    ) -> None:
        if not self.create_csv:
            return

        self.asset_movements_csv.append({
            'time':
            timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'exchange':
            str(exchange),
            'type':
            str(category),
            'moving_asset':
            asset.identifier,
            'fee_in_asset':
            fee,
            f'fee_in_{self.profit_currency.identifier}':
            fee * rate,
        })
        self.add_to_allevents(
            event_type=EV_ASSET_MOVE,
            location=exchange,
            paid_in_profit_currency=fee * rate,
            paid_asset=asset,
            paid_in_asset=fee,
            received_asset=S_EMPTYSTR,
            received_in_asset=FVal(0),
            taxable_received_in_profit_currency=FVal(0),
            timestamp=timestamp,
        )
Beispiel #15
0
 def serialize(
         self,
         export_data: Optional[Tuple[Asset,
                                     Price]] = None) -> Dict[str, str]:
     if export_data:
         return {
             'timestamp':
             timestamp_to_date(self.time, '%Y-%m-%d %H:%M:%S'),
             'category':
             self.category.serialize(),
             'asset':
             str(self.asset),
             'amount':
             self.amount,
             f'{export_data[0].symbol.lower()}_value':
             str(FVal(self.usd_value) * export_data[1]),  # noqa: 501
         }
     return {
         'timestamp': str(self.time),
         'category': self.category.serialize(),
         'asset_identifier': str(self.asset.identifier),
         'amount': self.amount,
         'usd_value': self.usd_value,
     }
Beispiel #16
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
Beispiel #17
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),
        )
Beispiel #18
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'),
        )
Beispiel #19
0
    def add_to_allevents(
        self,
        event_type: EventType,
        location: Location,
        paid_in_profit_currency: FVal,
        paid_asset: Union[Asset, EmptyStr],
        paid_in_asset: FVal,
        received_asset: Union[Asset, EmptyStr],
        received_in_asset: FVal,
        taxable_received_in_profit_currency: FVal,
        timestamp: Timestamp,
        is_virtual: bool = False,
        taxable_amount: FVal = ZERO,
        taxable_bought_cost: FVal = ZERO,
    ) -> None:
        row = len(self.all_events_csv) + 2
        if event_type == EV_BUY:
            net_profit_or_loss = FVal(0)  # no profit by buying
            net_profit_or_loss_csv = '0'
        elif event_type == EV_SELL:
            if taxable_amount == 0:
                net_profit_or_loss = FVal(0)
            else:
                net_profit_or_loss = taxable_received_in_profit_currency - taxable_bought_cost
            net_profit_or_loss_csv = '=IF(E{}=0,0,L{}-M{})'.format(
                row, row, row)
        elif event_type in (EV_TX_GAS_COST, EV_ASSET_MOVE, EV_LOAN_SETTLE):
            net_profit_or_loss = paid_in_profit_currency
            net_profit_or_loss_csv = '=-K{}'.format(row)
        elif event_type in (EV_INTEREST_PAYMENT, EV_MARGIN_CLOSE, EV_DEFI):
            net_profit_or_loss = taxable_received_in_profit_currency
            net_profit_or_loss_csv = '=L{}'.format(row)
        else:
            raise ValueError(
                'Illegal event type "{}" at add_to_allevents'.format(
                    event_type))

        exported_paid_asset = (paid_asset if isinstance(paid_asset, str) else
                               paid_asset.identifier)
        exported_received_asset = (received_asset if isinstance(
            received_asset, str) else received_asset.identifier)
        entry = {
            'type': event_type,
            'location': str(location),
            'paid_in_profit_currency': paid_in_profit_currency,
            'paid_asset': exported_paid_asset,
            'paid_in_asset': paid_in_asset,
            'taxable_amount': taxable_amount,
            'taxable_bought_cost_in_profit_currency': taxable_bought_cost,
            'received_asset': exported_received_asset,
            'taxable_received_in_profit_currency':
            taxable_received_in_profit_currency,
            'received_in_asset': received_in_asset,
            'net_profit_or_loss': net_profit_or_loss,
            'time': timestamp,
            'is_virtual': is_virtual,
        }
        log.debug('csv event', **make_sensitive(entry))
        self.all_events.append(entry)
        new_entry = entry.copy()
        new_entry['net_profit_or_loss'] = net_profit_or_loss_csv
        new_entry['time'] = timestamp_to_date(timestamp,
                                              formatstr='%d/%m/%Y %H:%M:%S')
        new_entry[
            f'paid_in_{self.profit_currency.identifier}'] = paid_in_profit_currency
        key = f'taxable_received_in_{self.profit_currency.identifier}'
        new_entry[key] = taxable_received_in_profit_currency
        key = f'taxable_bought_cost_in_{self.profit_currency.identifier}'
        new_entry[key] = taxable_bought_cost
        del new_entry['paid_in_profit_currency']
        del new_entry['taxable_received_in_profit_currency']
        del new_entry['taxable_bought_cost_in_profit_currency']
        self.all_events_csv.append(new_entry)
Beispiel #20
0
    def search_buys_calculate_profit(
        self,
        selling_amount: FVal,
        selling_asset: Asset,
        timestamp: Timestamp,
    ) -> Tuple[FVal, FVal, FVal]:
        """
        When selling `selling_amount` of `selling_asset` at `timestamp` this function
        calculates using the first-in-first-out rule the corresponding buy/s from
        which to do profit calculation. Also applies the one year rule after which
        a sell is not taxable in Germany.

        Returns a tuple of 3 values:
            - `taxable_amount`: The amount out of `selling_amount` that is taxable,
                                calculated from the 1 year rule.
            - `taxable_bought_cost`: How much it cost in `profit_currency` to buy
                                     the `taxable_amount`
            - `taxfree_bought_cost`: How much it cost in `profit_currency` to buy
                                     the taxfree_amount (selling_amount - taxable_amount)
        """
        remaining_sold_amount = selling_amount
        stop_index = -1
        taxfree_bought_cost = ZERO
        taxable_bought_cost = ZERO
        taxable_amount = ZERO
        taxfree_amount = ZERO
        remaining_amount_from_last_buy = FVal('-1')
        for idx, buy_event in enumerate(self.events[selling_asset].buys):
            if self.taxfree_after_period is None:
                at_taxfree_period = False
            else:
                at_taxfree_period = (buy_event.timestamp +
                                     self.taxfree_after_period < timestamp)

            if remaining_sold_amount < buy_event.amount:
                stop_index = idx
                buying_cost = remaining_sold_amount.fma(
                    buy_event.rate,
                    (buy_event.fee_rate * remaining_sold_amount),
                )

                if at_taxfree_period:
                    taxfree_amount += remaining_sold_amount
                    taxfree_bought_cost += buying_cost
                else:
                    taxable_amount += remaining_sold_amount
                    taxable_bought_cost += buying_cost

                remaining_amount_from_last_buy = buy_event.amount - remaining_sold_amount
                log.debug(
                    'Sell uses up part of historical buy',
                    sensitive_log=True,
                    tax_status='TAX-FREE' if at_taxfree_period else 'TAXABLE',
                    used_amount=remaining_sold_amount,
                    from_amount=buy_event.amount,
                    asset=selling_asset,
                    trade_buy_rate=buy_event.rate,
                    profit_currency=self.profit_currency,
                    trade_timestamp=buy_event.timestamp,
                )
                # stop iterating since we found all buys to satisfy this sell
                break
            else:
                buying_cost = buy_event.amount.fma(
                    buy_event.rate,
                    (buy_event.fee_rate * buy_event.amount),
                )
                remaining_sold_amount -= buy_event.amount
                if at_taxfree_period:
                    taxfree_amount += buy_event.amount
                    taxfree_bought_cost += buying_cost
                else:
                    taxable_amount += buy_event.amount
                    taxable_bought_cost += buying_cost

                log.debug(
                    'Sell uses up entire historical buy',
                    sensitive_log=True,
                    tax_status='TAX-FREE' if at_taxfree_period else 'TAXABLE',
                    bought_amount=buy_event.amount,
                    asset=selling_asset,
                    trade_buy_rate=buy_event.rate,
                    profit_currency=self.profit_currency,
                    trade_timestamp=buy_event.timestamp,
                )

                # If the sell used up the last historical buy
                if idx == len(self.events[selling_asset].buys) - 1:
                    stop_index = idx + 1

        if len(self.events[selling_asset].buys) == 0:
            log.critical(
                'No documented buy found for "{}" before {}'.format(
                    selling_asset,
                    timestamp_to_date(timestamp,
                                      formatstr='%d/%m/%Y %H:%M:%S'),
                ), )
            # That means we had no documented buy for that asset. This is not good
            # because we can't prove a corresponding buy and as such we are burdened
            # calculating the entire sell as profit which needs to be taxed
            return selling_amount, ZERO, ZERO

        # Otherwise, delete all the used up buys from the list
        del self.events[selling_asset].buys[:stop_index]
        # and modify the amount of the buy where we stopped if there is one
        if remaining_amount_from_last_buy != FVal('-1'):
            self.events[selling_asset].buys[
                0].amount = remaining_amount_from_last_buy
        elif remaining_sold_amount != ZERO:
            # if we still have sold amount but no buys to satisfy it then we only
            # found buys to partially satisfy the sell
            adjusted_amount = selling_amount - taxfree_amount
            log.critical(
                'Not enough documented buys found for "{}" before {}.'
                'Only found buys for {} {}'.format(
                    selling_asset,
                    timestamp_to_date(timestamp,
                                      formatstr='%d/%m/%Y %H:%M:%S'),
                    taxable_amount + taxfree_amount,
                    selling_asset,
                ), )
            return adjusted_amount, taxable_bought_cost, taxfree_bought_cost

        return taxable_amount, taxable_bought_cost, taxfree_bought_cost
Beispiel #21
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
Beispiel #22
0
    def _can_sync_data_from_server(self, new_account: bool) -> SyncCheckResult:
        """
        Checks if the remote data can be pulled from the server.

        Returns a SyncCheckResult denoting whether we can pull for sure,
        whether we can't pull or whether the user should be asked. If the user
        should be asked a message is also returned
        """
        log.debug('can sync data from server -- start')
        if self.premium is None:
            return SyncCheckResult(can_sync=CanSync.NO,
                                   message='',
                                   payload=None)

        b64_encoded_data, our_hash = self.data.compress_and_encrypt_db(
            self.password)

        try:
            metadata = self.premium.query_last_data_metadata()
        except RemoteError as e:
            log.debug('can sync data from server failed', error=str(e))
            return SyncCheckResult(can_sync=CanSync.NO,
                                   message='',
                                   payload=None)

        if new_account:
            return SyncCheckResult(can_sync=CanSync.YES,
                                   message='',
                                   payload=None)

        if not self.data.db.get_premium_sync():
            # If it's not a new account and the db setting for premium syncing is off stop
            return SyncCheckResult(can_sync=CanSync.NO,
                                   message='',
                                   payload=None)

        log.debug(
            'CAN_PULL',
            ours=our_hash,
            theirs=metadata.data_hash,
        )
        if our_hash == metadata.data_hash:
            log.debug('sync from server stopped -- same hash')
            # same hash -- no need to get anything
            return SyncCheckResult(can_sync=CanSync.NO,
                                   message='',
                                   payload=None)

        our_last_write_ts = self.data.db.get_last_write_ts()
        data_bytes_size = len(base64.b64decode(b64_encoded_data))

        local_more_recent = our_last_write_ts >= metadata.last_modify_ts
        local_bigger = data_bytes_size >= metadata.data_size

        if local_more_recent and local_bigger:
            log.debug(
                'sync from server stopped -- local is both newer and bigger')
            return SyncCheckResult(can_sync=CanSync.NO,
                                   message='',
                                   payload=None)

        if local_more_recent is False:  # remote is more recent
            message = (
                'Detected remote database with more recent modification timestamp '
                'than the local one. ')
        else:  # remote is bigger
            message = 'Detected remote database with bigger size than the local one. '

        return SyncCheckResult(
            can_sync=CanSync.ASK_USER,
            message=message,
            payload={
                'local_size': data_bytes_size,
                'remote_size': metadata.data_size,
                'local_last_modified': timestamp_to_date(our_last_write_ts),
                'remote_last_modified':
                timestamp_to_date(metadata.last_modify_ts),
            },
        )
Beispiel #23
0
    def add_sell(
        self,
        location: Location,
        selling_asset: Asset,
        rate_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        gain_in_profit_currency: FVal,
        selling_amount: FVal,
        receiving_asset: Optional[Asset],
        receiving_amount: Optional[FVal],
        receiving_asset_rate_in_profit_currency: FVal,
        taxable_amount: FVal,
        taxable_bought_cost: FVal,
        timestamp: Timestamp,
        is_virtual: bool,
    ) -> None:
        if not self.create_csv:
            return

        processed_receiving_asset: Union[EmptyStr, Asset] = (
            EmptyStr('') if receiving_asset is None else receiving_asset)
        exported_receiving_asset = '' if receiving_asset is None else receiving_asset.identifier
        processed_receiving_amount = FVal(
            0) if not receiving_amount else receiving_amount
        exchange_rate_key = f'exchanged_asset_{self.profit_currency.identifier}_exchange_rate'
        taxable_profit_received = 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,
        )
        row = len(self.trades_csv) + 2
        taxable_profit_formula = '=IF(H{}=0,0,L{}-K{})'.format(row, row, row)
        self.trades_csv.append({
            'type':
            'sell',
            'location':
            str(location),
            'asset':
            selling_asset.identifier,
            f'price_in_{self.profit_currency.identifier}':
            rate_in_profit_currency,
            f'fee_in_{self.profit_currency.identifier}':
            total_fee_in_profit_currency,
            f'gained_or_invested_{self.profit_currency.identifier}':
            gain_in_profit_currency,
            'amount':
            selling_amount,
            'taxable_amount':
            taxable_amount,
            'exchanged_for':
            exported_receiving_asset,
            exchange_rate_key:
            receiving_asset_rate_in_profit_currency,
            f'taxable_bought_cost_in_{self.profit_currency.identifier}':
            taxable_bought_cost,
            f'taxable_gain_in_{self.profit_currency.identifier}':
            taxable_profit_received,
            f'taxable_profit_loss_in_{self.profit_currency.identifier}':
            taxable_profit_formula,
            'time':
            timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'is_virtual':
            is_virtual,
        })
        paid_in_profit_currency = ZERO
        self.add_to_allevents(
            event_type=EV_SELL,
            location=location,
            paid_in_profit_currency=paid_in_profit_currency,
            paid_asset=selling_asset,
            paid_in_asset=selling_amount,
            received_asset=processed_receiving_asset,
            received_in_asset=processed_receiving_amount,
            taxable_received_in_profit_currency=taxable_profit_received,
            timestamp=timestamp,
            is_virtual=is_virtual,
            taxable_amount=taxable_amount,
            taxable_bought_cost=taxable_bought_cost,
        )
Beispiel #24
0
def configure_logging(args: argparse.Namespace) -> None:
    loglevel = args.loglevel.upper()
    formatters = {
        'default': {
            'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s',
            'datefmt': '%d/%m/%Y %H:%M:%S %Z',
        },
    }
    handlers = {
        'console': {
            'class': 'logging.StreamHandler',
            'level': loglevel,
            'formatter': 'default',
        },
    }

    if args.max_logfiles_num < 0:
        backups_num = 0
    else:
        backups_num = args.max_logfiles_num - 1

    if args.logtarget == 'file':
        given_filepath = Path(args.logfile)
        filepath = given_filepath
        if not getattr(sys, 'frozen', False):
            # not packaged -- must be in develop mode. Append date to each file
            date = timestamp_to_date(
                ts=ts_now(),
                formatstr='%Y%m%d_%H%M%S',
                treat_as_local=True,
            )
            filepath = given_filepath.parent / f'{date}_{given_filepath.name}'

        selected_handlers = ['file']
        single_log_max_bytes = int(
            (args.max_size_in_mb_all_logs * 1024 * 1000) / args.max_logfiles_num,
        )
        handlers['file'] = {
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': filepath,
            'mode': 'a',
            'maxBytes': single_log_max_bytes,
            'backupCount': backups_num,
            'level': loglevel,
            'formatter': 'default',
        }
    else:
        selected_handlers = ['console']

    filters = {
        'pywsgi': {
            '()': PywsgiFilter,
        },
    }
    loggers = {
        '': {  # root logger
            'level': loglevel,
            'handlers': selected_handlers,
        },
        'rotkehlchen.api.server.pywsgi': {
            'level': loglevel,
            'handlers': selected_handlers,
            'filters': ['pywsgi'],
            'propagate': False,
        },
    }
    logging.config.dictConfig({
        'version': 1,
        'disable_existing_loggers': False,
        'filters': filters,
        'formatters': formatters,
        'handlers': handlers,
        'loggers': loggers,
    })

    if not args.logfromothermodules:
        logging.getLogger('urllib3').setLevel(logging.CRITICAL)
        logging.getLogger('urllib3.connectionpool').setLevel(logging.CRITICAL)
        logging.getLogger('substrateinterface.base').setLevel(logging.CRITICAL)
Beispiel #25
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
Beispiel #26
0
    def add_sell(
        self,
        selling_asset: Asset,
        rate_in_profit_currency: FVal,
        total_fee_in_profit_currency: Fee,
        gain_in_profit_currency: FVal,
        selling_amount: FVal,
        receiving_asset: Asset,
        receiving_amount: FVal,
        receiving_asset_rate_in_profit_currency: FVal,
        taxable_amount: FVal,
        taxable_bought_cost: FVal,
        timestamp: Timestamp,
        is_virtual: bool,
    ):
        if not self.create_csv:
            return

        exchange_rate_key = f'exchanged_asset_{self.profit_currency}_exchange_rate'
        taxable_profit_received = 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,
        )
        row = len(self.trades_csv) + 2
        taxable_profit_formula = '=IF(G{}=0,0,K{}-J{})'.format(row, row, row)
        self.trades_csv.append({
            'type':
            'sell',
            'asset':
            selling_asset,
            'price_in_{}'.format(self.profit_currency):
            rate_in_profit_currency,
            'fee_in_{}'.format(self.profit_currency):
            total_fee_in_profit_currency,
            'gained_or_invested_{}'.format(self.profit_currency):
            gain_in_profit_currency,
            'amount':
            selling_amount,
            'taxable_amount':
            taxable_amount,
            'exchanged_for':
            receiving_asset,
            exchange_rate_key:
            receiving_asset_rate_in_profit_currency,
            'taxable_bought_cost_in_{}'.format(self.profit_currency):
            taxable_bought_cost,
            'taxable_gain_in_{}'.format(self.profit_currency):
            taxable_profit_received,
            'taxable_profit_loss_in_{}'.format(self.profit_currency):
            taxable_profit_formula,
            'time':
            timestamp_to_date(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
            'is_virtual':
            is_virtual,
        })
        paid_in_profit_currency = ZERO
        self.add_to_allevents(
            event_type=EV_SELL,
            paid_in_profit_currency=paid_in_profit_currency,
            paid_asset=selling_asset,
            paid_in_asset=selling_amount,
            received_asset=receiving_asset,
            received_in_asset=receiving_amount,
            taxable_received_in_profit_currency=taxable_profit_received,
            timestamp=timestamp,
            is_virtual=is_virtual,
            taxable_amount=taxable_amount,
            taxable_bought_cost=taxable_bought_cost,
        )
Beispiel #27
0
 def timestamp_to_date(self, timestamp: Timestamp) -> str:
     return timestamp_to_date(
         timestamp,
         formatstr=self.dateformat,
         treat_as_local=self.datelocaltime,
     )
Beispiel #28
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
        """
        # TODO: Figure out a better way to log and return. Only thing I can imagine
        # is nested ifs (ugly af) or a different function (meh + performance).

        # 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):
            log.debug('Got historical price from cryptocompare',
                      from_asset=from_asset,
                      to_asset=to_asset,
                      timestamp=timestamp,
                      price=price)  # noqa: E501
            return price

        # check DB cache
        price_cache_entry = GlobalDBHandler().get_historical_price(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            max_seconds_distance=3600,
            source=HistoricalPriceOracle.CRYPTOCOMPARE,
        )
        if price_cache_entry and price_cache_entry.price != Price(ZERO):
            log.debug('Got historical price from cryptocompare',
                      from_asset=from_asset,
                      to_asset=to_asset,
                      timestamp=timestamp,
                      price=price)  # noqa: E501
            return price_cache_entry.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)
        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',
                    treat_as_local=True,
                ),
            )

        log.debug('Got historical price from cryptocompare',
                  from_asset=from_asset,
                  to_asset=to_asset,
                  timestamp=timestamp,
                  price=price)  # noqa: E501
        return price
Beispiel #29
0
    def query_historical_price(
        self,
        from_asset: Asset,
        to_asset: Asset,
        timestamp: Timestamp,
    ) -> Price:
        vs_currency = Coingecko.check_vs_currencies(
            from_asset=from_asset,
            to_asset=to_asset,
            location='historical price',
        )
        if not vs_currency:
            return Price(ZERO)

        try:
            from_coingecko_id = from_asset.to_coingecko()
        except UnsupportedAsset:
            log.warning(
                f'Tried to query coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But from_asset is not supported in coingecko',
            )
            return Price(ZERO)

        # check DB cache
        price_cache_entry = GlobalDBHandler().get_historical_price(
            from_asset=from_asset,
            to_asset=to_asset,
            timestamp=timestamp,
            max_seconds_distance=DAY_IN_SECONDS,
            source=HistoricalPriceOracle.COINGECKO,
        )
        if price_cache_entry:
            return price_cache_entry.price

        # no cache, query coingecko for daily price
        date = timestamp_to_date(timestamp, formatstr='%d-%m-%Y')
        result = self._query(
            module='coins',
            subpath=f'{from_coingecko_id}/history',
            options={
                'date': date,
                'localization': False,
            },
        )

        try:
            price = Price(
                FVal(result['market_data']['current_price'][vs_currency]))
        except KeyError as e:
            log.warning(
                f'Queried coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But got key error for {str(e)} when '
                f'processing the result.', )
            return Price(ZERO)

        # save result in the DB and return
        date_timestamp = create_timestamp(date, formatstr='%d-%m-%Y')
        GlobalDBHandler().add_historical_prices(entries=[
            HistoricalPrice(
                from_asset=from_asset,
                to_asset=to_asset,
                source=HistoricalPriceOracle.COINGECKO,
                timestamp=date_timestamp,
                price=price,
            )
        ])
        return price
Beispiel #30
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