def import_transaction( account: models.Account, fiat_record: pd.Series, token_record: pd.Series, ) -> Tuple[models.Transaction, bool]: raw_record = _to_raw_record((fiat_record, token_record)) executed_at = _parse_utc_datetime(fiat_record["UTC_Time"]) symbol = token_record["Coin"] fiat_currency = fiat_record["Coin"] raw_fiat_value = to_decimal(fiat_record["Change"]) fiat_value = raw_fiat_value from_currency = models.currency_enum_from_string(fiat_currency) if fiat_currency != models.Currency(account.currency).label: to_currency = account.currency exchange_rate = prices.get_closest_exchange_rate( executed_at.date(), from_currency, to_currency ) if exchange_rate is None: raise CurrencyMismatch( "Couldn't convert the fiat to account currency, missing exchange rate" ) else: fiat_value *= exchange_rate.value raw_record += f" exchange rate: {exchange_rate.value}" if fiat_currency == "USD": fiat_value_usd = raw_fiat_value else: to_currency = models.Currency.USD exchange_rate = prices.get_closest_exchange_rate( executed_at.date(), from_currency, to_currency ) if exchange_rate is None: raise CurrencyMismatch( "Couldn't convert the fiat to USD, missing exchange rate" ) fiat_value_usd = raw_fiat_value * exchange_rate.value quantity = to_decimal(token_record["Change"]) with decimal.localcontext() as c: c.prec = 10 price = decimal.Decimal(-fiat_value_usd / quantity) return ( *accounts.AccountRepository().add_transaction_crypto_asset( account, symbol, executed_at, quantity, price, fiat_value_usd, fiat_value, fiat_value, ), raw_record, )
def compute_price(self, account, to_currency, arguments): price_in_account_currency = ( arguments["value_in_account_currency"] / arguments["quantity"] ) from_currency = account.currency if from_currency == to_currency: raise serializers.ValidationError( { "price": [ "Price required if the asset is traded in the account currency." ], } ) date = arguments["executed_at"].date() exchange_rate = prices.get_closest_exchange_rate( date, from_currency, to_currency ) if exchange_rate is None: raise serializers.ValidationError( { "price": [ "Please provide the price, no suitable exchange rate available." ], } ) return price_in_account_currency * exchange_rate.value
def delete_event(self, event: models.AccountEvent) -> None: account = event.account # This makes sense for all currently supported events, # but might not in the future. balance_change = event.amount if event.withheld_taxes: balance_change -= event.withheld_taxes if event.event_type == models.EventType.DIVIDEND and event.position: position_currency = event.position.asset.currency account_currency = account.currency if position_currency != account_currency: exchange_rate = prices.get_closest_exchange_rate( date=event.executed_at.date(), from_currency=position_currency, to_currency=account_currency, ) if exchange_rate is None: raise ValueError( f"Can't convert between currencies: " f"{position_currency} and {account_currency}") balance_change *= exchange_rate.value account.balance -= balance_change account.save() transaction = event.transaction event.delete() if transaction: self.delete_transaction(transaction)
def test_exchange_too_late(self): rate = prices.get_closest_exchange_rate( date=datetime.date.fromisoformat("2021-11-03"), from_currency=self.from_currency, to_currency=self.to_currency, ) # Will use last date available. self.assertEqual(rate.date, datetime.date.fromisoformat("2021-05-01")) self.assertEqual(rate.from_currency, self.from_currency) self.assertEqual(rate.to_currency, self.to_currency) self.assertEqual(rate.value, decimal.Decimal("0.8"))
def test_exchange_too_early(self): rate = prices.get_closest_exchange_rate( date=datetime.date.fromisoformat("2000-04-03"), from_currency=self.from_currency, to_currency=self.to_currency, ) # Will use first date available. self.assertEqual(rate.date, datetime.date.fromisoformat("2020-03-02")) self.assertEqual(rate.from_currency, self.from_currency) self.assertEqual(rate.to_currency, self.to_currency) self.assertEqual(rate.value, decimal.Decimal("1.8"))
def test_exchange_rate_sparse_range(self): rate = prices.get_closest_exchange_rate( date=datetime.date.fromisoformat("2020-04-03"), from_currency=self.from_currency, to_currency=self.to_currency, ) # Should use first date before it. self.assertEqual(rate.date, datetime.date.fromisoformat("2020-04-01")) self.assertEqual(rate.from_currency, self.from_currency) self.assertEqual(rate.to_currency, self.to_currency) self.assertEqual(rate.value, decimal.Decimal("1.8"))
def test_exchange_rate_present(self): rate = prices.get_closest_exchange_rate( date=datetime.date.fromisoformat("2021-04-03"), from_currency=self.from_currency, to_currency=self.to_currency, ) self.assertEqual(rate.date, datetime.date.fromisoformat("2021-04-03")) self.assertEqual(rate.from_currency, self.from_currency) self.assertEqual(rate.to_currency, self.to_currency) self.assertEqual(rate.value, decimal.Decimal("1.1"))
def _convert_usd_to_account_currency( value: decimal.Decimal, account: models.Account, date: datetime.date ) -> decimal.Decimal: if account.currency == models.Currency.USD: return value from_currency = account.currency to_currency = models.Currency.USD exchange_rate = prices.get_closest_exchange_rate(date, from_currency, to_currency) if exchange_rate is None: raise CurrencyMismatch( "Couldn't convert USD to account currency, missing exchange rate" ) return value * exchange_rate.value
def add_event( self, account: models.Account, amount: decimal.Decimal, executed_at: datetime.datetime, event_type: models.EventType, position: Optional[models.Position] = None, withheld_taxes: Optional[decimal.Decimal] = None, ) -> Tuple[models.AccountEvent, bool]: if (event_type == models.EventType.DEPOSIT or event_type == models.EventType.DIVIDEND): assert amount > 0 if event_type == models.EventType.WITHDRAWAL: assert amount < 0 event, created = models.AccountEvent.objects.get_or_create( account=account, amount=amount, executed_at=executed_at, event_type=event_type, position=position, withheld_taxes=withheld_taxes or 0, ) if created: balance_change = amount if withheld_taxes: balance_change -= withheld_taxes if event_type == models.EventType.DIVIDEND and position: position_currency = position.asset.currency account_currency = account.currency if position_currency != account_currency: exchange_rate = prices.get_closest_exchange_rate( date=executed_at.date(), from_currency=position_currency, to_currency=account_currency, ) if exchange_rate is None: raise ValueError( f"Can't convert between currencies: " f"{position_currency} and {account_currency}") balance_change *= exchange_rate.value account.balance += balance_change account.save() return event, created
def import_fiat_transfers(account, records): account_repository = accounts.AccountRepository() successful_records = [] for record in records.iloc: raw_record = record.to_csv() event_type = models.EventType.DEPOSIT if record["Operation"] == "Withdrawal": event_type = models.EventType.WITHDRAWAL executed_at = _parse_utc_datetime(record["UTC_Time"]) fiat_currency = record["Coin"] fiat_value = to_decimal(record["Change"]) if fiat_currency != models.Currency(account.currency).label: from_currency = models.currency_enum_from_string(fiat_currency) to_currency = account.currency exchange_rate = prices.get_closest_exchange_rate( executed_at.date(), from_currency, to_currency ) if exchange_rate is None: raise CurrencyMismatch( "Couldn't convert the fiat to account currency, missing exchange rate" ) else: fiat_value *= exchange_rate.value raw_record += f", exchange rate: {exchange_rate.value}" event, created = account_repository.add_event( account, amount=fiat_value, executed_at=executed_at, event_type=event_type, ) successful_records.append( { "record": raw_record, "event": event, "transaction": None, "created": created, } ) return successful_records