Beispiel #1
0
 def test_deleting_transaction_fails_if_transaction_has_event(self):
     self.assertEqual(models.Position.objects.count(), 1)
     first_transaction = models.Transaction.objects.last()
     event, _ = accounts.AccountRepository().add_crypto_income_event(
         self.account,
         self.isin,
         first_transaction.executed_at,
         10,
         200,
         2000,
         2100,
         models.EventType.STAKING_INTEREST,
     )
     self.assertEqual(models.Transaction.objects.count(), 9)
     response = self.client.delete(
         reverse(self.VIEW_NAME, args=[event.transaction.pk])
     )
     self.assertEqual(response.status_code, 400)
     self.assertEqual(
         response.json(),
         [
             "Can't delete a transaction associated with an event, without deleting the event first."
         ],
     )
     self.assertEqual(models.Transaction.objects.count(), 9)
Beispiel #2
0
def _add_transaction(
    account, isin, exchange, executed_at, quantity, price, add_price_history=True
):
    transaction_costs = decimal.Decimal(0.5)
    local_value = decimal.Decimal(0.5)
    value_in_account_currency = decimal.Decimal(0.5)
    total_in_account_currency = decimal.Decimal(0.5)
    order_id = "123"
    account_repository = accounts.AccountRepository()
    transaction, _ = account_repository.add_transaction(
        account,
        isin,
        exchange,
        executed_at,
        quantity,
        price,
        transaction_costs,
        local_value,
        value_in_account_currency,
        total_in_account_currency,
        order_id,
        asset_defaults={"local_currency": "USD", "name": isin},
        import_all_assets=True,
    )
    asset = transaction.position.asset
    if add_price_history:
        models.PriceHistory.objects.create(
            asset=asset, value=price, date=transaction.executed_at.date()
        )
        models.CurrencyExchangeRate.objects.create(
            from_currency=models.Currency.USD,
            to_currency=models.Currency.EUR,
            value=0.84,
            date="2020-02-03",
        )
Beispiel #3
0
def _add_transaction(account, isin, exchange, executed_at, quantity, price):
    transaction_costs = decimal.Decimal(0.5)
    local_value = decimal.Decimal(0.5)
    value_in_account_currency = decimal.Decimal(0.5)
    total_in_account_currency = decimal.Decimal(0.5)
    order_id = "123"
    account_repository = accounts.AccountRepository()
    account_repository.add_transaction(
        account,
        isin,
        exchange,
        executed_at,
        quantity,
        price,
        transaction_costs,
        local_value,
        value_in_account_currency,
        total_in_account_currency,
        order_id,
        asset_defaults={
            "local_currency": "USD",
            "name": isin
        },
        import_all_assets=True,
    )
Beispiel #4
0
    def create(self, request):
        serializer = self.get_serializer(
            data=request.data, context=self.get_serializer_context()
        )
        serializer.is_valid(raise_exception=True)
        assert isinstance(self.request.user, User)
        self.request.user

        arguments = serializer.validated_data.copy()

        try:
            account_repository = accounts.AccountRepository()
            account = account_repository.get(
                user=self.request.user, id=serializer.validated_data["account"]
            )
        except models.Account.DoesNotExist:
            raise exceptions.PermissionDenied(
                detail={
                    "account": "Current user doesn't have access to this account or it doesn't exist."
                }
            )
        try:
            transaction_import = binance_parser.import_transactions_from_file(
                account, arguments["transaction_file"]
            )
        except binance_parser.InvalidFormat as e:
            return Response(
                status=status.HTTP_400_BAD_REQUEST, data={"transaction_file": e.args[0]}
            )

        serializer = TransactionImportSerializer(
            instance=transaction_import, context=self.get_serializer_context()
        )
        return Response(status=status.HTTP_201_CREATED, data=serializer.data)
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 import_transaction(
    account: models.Account,
    transaction_record: pd.Series,
    import_all_assets,
) -> Tuple[models.Transaction, bool]:
    executed_at = transaction_record["Datetime"]
    isin = transaction_record["ISIN"]
    local_value = transaction_record["Local value"]
    total_in_account_currency = transaction_record["Total"]
    value_in_account_currency = transaction_record["Value"]
    transaction_costs = transaction_record["Transaction costs"].astype(str)
    order_id = transaction_record["Order ID"]
    quantity = transaction_record["Quantity"]
    price = transaction_record["Price"]
    account_currency = transaction_record["Value currency"]
    local_currency = transaction_record["Local value currency"]
    product = transaction_record["Product"]

    if models.currency_enum_from_string(account_currency) != account.currency:
        raise CurrencyMismatch("Currency of import didn't match the account")
    exchange_mic = transaction_record["Venue"]
    exchange_ref = transaction_record["Reference"]
    try:
        exchange = stock_exchanges.ExchangeRepository().get(
            exchange_mic, exchange_ref)
    except Exception as e:
        logger.error(e)
        raise e

    def to_decimal(pd_f):
        return decimal.Decimal(pd_f.astype(str))

    transaction_costs = decimal.Decimal(transaction_costs)
    if transaction_costs.is_nan():
        transaction_costs = None
    return accounts.AccountRepository().add_transaction(
        account,
        isin=isin,
        exchange=exchange,
        executed_at=executed_at,
        quantity=to_decimal(quantity),
        price=to_decimal(price),
        transaction_costs=transaction_costs,
        local_value=to_decimal(local_value),
        value_in_account_currency=to_decimal(value_in_account_currency),
        total_in_account_currency=to_decimal(total_in_account_currency),
        order_id=order_id,
        asset_defaults={
            "local_currency": local_currency,
            "name": product
        },
        import_all_assets=import_all_assets,
    )
Beispiel #7
0
def _add_account_event(account, event_type, amount, executed_at=None, position=None):
    if executed_at is None:
        executed_at = timezone.now()

    account_repository = accounts.AccountRepository()
    account_repository.add_event(
        account,
        amount=amount,
        executed_at=executed_at,
        event_type=event_type,
        position=position,
    )
Beispiel #8
0
 def create(self, request, *args, **kwargs):
     serializer = self.get_serializer(
         data=request.data, context=self.get_serializer_context()
     )
     serializer.is_valid(raise_exception=True)
     assert isinstance(self.request.user, User)
     accounts.AccountRepository().create(
         user=self.request.user, **serializer.validated_data
     )
     headers = self.get_success_headers(serializer.data)
     return Response(
         serializer.data, status=status.HTTP_201_CREATED, headers=headers
     )
Beispiel #9
0
 def perform_destroy(self, instance):
     account_repository = accounts.AccountRepository()
     try:
         account_repository.delete_transaction(instance)
     except gains.SoldBeforeBought:
         raise serializers.ValidationError(
             {
                 "quantity": ["Can't sell asset before buying it."],
             }
         )
     except accounts.CantModifyTransactionWithEvent:
         raise serializers.ValidationError(
             "Can't delete a transaction associated with an event, without deleting the event first."
         )
Beispiel #10
0
    def handle(self, *args, **options):
        account_id = options['account_id']
        username = options['username']
        user = User.objects.get(username=username)
        account = accounts.AccountRepository().get(user, account_id)
        filename = options['filename']
        failed_rows = degiro_parser.import_transactions_from_file(
            account, filename)

        self.stdout.write(self.style.SUCCESS('Finished the import'))
        if failed_rows:
            self.stderr.write('Failed rows:')
            for row in failed_rows:
                self.stderr.write(
                    f"Failed to import ISIN: {row['ISIN']}, exchange: {row['Reference']} {row['Venue']}"
                )
Beispiel #11
0
 def add_crypto_income(self, request):
     serializer = self.get_serializer(
         data=request.data, context=self.get_serializer_context()
     )
     serializer.is_valid(raise_exception=True)
     assert isinstance(self.request.user, User)
     account_repository = accounts.AccountRepository()
     arguments = serializer.validated_data.copy()
     arguments["local_value"] = -arguments["local_value"]
     arguments["value_in_account_currency"] = -arguments["value_in_account_currency"]
     event, _ = account_repository.add_crypto_income_event(**arguments)
     asset = event.transaction.position.asset
     if asset.tracked:
         tasks.collect_prices.delay(asset.pk)
     headers = self.get_success_headers(serializer.data)
     data = serializer.data
     data["id"] = event.id
     return Response(data, status=status.HTTP_201_CREATED, headers=headers)
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
Beispiel #13
0
 def create(self, request, *args, **kwargs):
     serializer = self.get_serializer(
         data=request.data, context=self.get_serializer_context()
     )
     serializer.is_valid(raise_exception=True)
     assert isinstance(self.request.user, User)
     account_repository = accounts.AccountRepository()
     arguments = serializer.validated_data.copy()
     if arguments["event_type"] in models.EVENT_TYPES_FOR_CRYPTO_INCOME:
         raise serializers.ValidationError(
             {
                 "event_type": "Crypto income events not supported in this API endpoint, '/add_crypto_event' instead"
             }
         )
     event, _ = account_repository.add_event(**arguments)
     headers = self.get_success_headers(serializer.data)
     data = serializer.data
     data["id"] = event.id
     return Response(data, status=status.HTTP_201_CREATED, headers=headers)
Beispiel #14
0
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(
            data=request.data, context=self.get_serializer_context()
        )
        serializer.is_valid(raise_exception=True)
        assert isinstance(self.request.user, User)
        account_repository = accounts.AccountRepository()
        account = account_repository.get(
            user=self.request.user, id=serializer.validated_data["account"]
        )

        arguments = serializer.validated_data.copy()
        arguments.pop("account")
        asset_id = arguments.pop("asset")
        arguments["asset_id"] = asset_id
        if (
            arguments.get("price", None) is None
            or arguments.get("local_value", None) is None
        ):
            to_currency = models.Asset.objects.get(pk=asset_id).currency
            arguments["price"] = self.compute_price(account, to_currency, arguments)
            arguments["local_value"] = arguments["price"] * arguments["quantity"]
        try:
            transaction = account_repository.add_transaction_known_asset(
                account, **arguments
            )
            asset = transaction.position.asset
            if asset.tracked:
                tasks.collect_prices.delay(asset.pk)
        except gains.SoldBeforeBought:
            raise serializers.ValidationError(
                {
                    "quantity": ["Can't sell asset before buying it."],
                }
            )
        headers = self.get_success_headers(serializer.data)
        data = serializer.data
        data["id"] = transaction.pk
        data["price"] = str(transaction.price)
        data["local_value"] = str(transaction.local_value)
        return Response(data, status=status.HTTP_201_CREATED, headers=headers)
Beispiel #15
0
    def add_with_custom_asset(self, request):
        serializer = self.get_serializer(
            data=request.data, context=self.get_serializer_context()
        )
        serializer.is_valid(raise_exception=True)
        assert isinstance(self.request.user, User)
        account_repository = accounts.AccountRepository()
        account = account_repository.get(
            user=self.request.user, id=serializer.validated_data["account"]
        )
        arguments = serializer.validated_data.copy()
        arguments.pop("account")

        if (
            arguments.get("price", None) is None
            or arguments.get("local_value", None) is None
        ):
            to_currency = arguments["currency"]
            arguments["price"] = self.compute_price(account, to_currency, arguments)
            arguments["local_value"] = arguments["price"] * arguments["quantity"]
        try:
            transaction = account_repository.add_transaction_custom_asset(
                account, **arguments
            )
        except gains.SoldBeforeBought:
            raise serializers.ValidationError(
                {
                    "quantity": ["Can't sell asset before buying it."],
                }
            )

        headers = self.get_success_headers(serializer.data)
        data = serializer.data
        data["id"] = transaction.pk
        data["price"] = str(transaction.price)
        data["local_value"] = str(transaction.local_value)
        return Response(data, status=status.HTTP_201_CREATED, headers=headers)
Beispiel #16
0
 def perform_update(self, serializer):
     account_repository = accounts.AccountRepository()
     try:
         account_repository.update(serializer)
     except accounts.CantUpdateNonEmptyAccount:
         raise serializers.ValidationError("can't update non-empty account")
Beispiel #17
0
    def test_lots_based_on_transactions(self):
        transaction_costs = decimal.Decimal("-0.5")
        local_value = decimal.Decimal("-12.2")
        value_in_account_currency = decimal.Decimal("-10.5")
        total_in_account_currency = decimal.Decimal(-11)
        quantity = 10
        price = decimal.Decimal("1.22")
        order_id = "123"
        executed_at = "2021-04-27 10:00Z"
        account_repository = accounts.AccountRepository()
        account_repository.add_transaction(
            self.account,
            self.isin,
            self.exchange,
            executed_at,
            quantity,
            price,
            transaction_costs,
            local_value,
            value_in_account_currency,
            total_in_account_currency,
            order_id,
            asset_defaults={"local_currency": "USD"},
            import_all_assets=False,
        )

        self.assertEqual(models.Lot.objects.count(), 1)
        lot = models.Lot.objects.first()
        self.assertEqual(lot.quantity, quantity)
        self.assertEqual(lot.buy_price, price)
        self.assertEqual(lot.cost_basis_account_currency,
                         total_in_account_currency)

        executed_at = "2021-04-27 11:00Z"

        transaction, _ = account_repository.add_transaction(
            self.account,
            self.isin,
            self.exchange,
            executed_at,
            -7,
            decimal.Decimal("3.22"),
            transaction_costs,
            decimal.Decimal("22.54"),
            decimal.Decimal("20.54"),
            decimal.Decimal("20.04"),
            order_id,
            asset_defaults={"local_currency": "USD"},
            import_all_assets=False,
        )

        self.assertEqual(models.Lot.objects.count(), 2)
        lots = models.Lot.objects.order_by("id").all()

        self.assertEqual(lots[0].quantity, 7)
        self.assertEqual(lots[1].quantity, 3)

        # -11 * 0.7 + 20.04 = 12.34
        self.assertEqual(lots[0].realized_gain_account_currency,
                         decimal.Decimal("12.34"))

        account_repository.correct_transaction(transaction, {"quantity": -5})

        self.assertEqual(models.Lot.objects.count(), 2)
        lots = models.Lot.objects.order_by("id").all()

        self.assertEqual(lots[0].quantity, 5)
        self.assertEqual(lots[1].quantity, 5)

        account_repository.delete_transaction(transaction)
        self.assertEqual(models.Lot.objects.count(), 1)
        lots = models.Lot.objects.order_by("id").all()

        self.assertEqual(lots[0].quantity, 10)
        self.assertEqual(lots[0].realized_gain_account_currency, None)
def import_income_transactions(account: models.Account, records: pd.DataFrame):
    successful_records = []
    failed_records = []

    for record in records.iloc:
        raw_record = record.to_csv()
        executed_at = _parse_utc_datetime(record["UTC_Time"])
        executed_at_date = executed_at.date()
        symbol = record["Coin"]
        quantity = to_decimal(record["Change"])

        if record["Operation"] == "POS savings interest":
            event_type = models.EventType.STAKING_INTEREST
        elif record["Operation"] == "Savings Interest":
            event_type = models.EventType.SAVINGS_INTEREST
        elif record["Operation"] == "ETH 2.0 Staking Rewards":
            event_type = models.EventType.STAKING_INTEREST
            # In binance, ETH is exchanged for BETH, but it's actually ETH.
            symbol = "ETH"
        else:
            raise ValueError("Unsupported Operation")

        try:
            price = prices.get_crypto_usd_price_at_date(symbol, date=executed_at_date)

            fiat_value_usd = -quantity * price
            fiat_value = _convert_usd_to_account_currency(
                fiat_value_usd, account, executed_at_date
            )

            event, created = accounts.AccountRepository().add_crypto_income_event(
                account,
                symbol,
                executed_at,
                quantity,
                price,
                fiat_value_usd,
                fiat_value,
                event_type,
            )
            successful_records.append(
                {
                    "record": raw_record,
                    "event": event,
                    "transaction": event.transaction,
                    "created": created,
                }
            )
        except prices.PriceNotAvailable as e:
            failed_records.append(
                {
                    "record": raw_record,
                    "issue": str(e),
                    "issue_type": models.ImportIssueType.FAILED_TO_FETCH_PRICE,
                }
            )
        except Exception as e:
            failed_records.append(
                {
                    "record": raw_record,
                    "issue": str(e),
                    "issue_type": models.ImportIssueType.UNKNOWN_FAILURE,
                }
            )
    return successful_records, failed_records
Beispiel #19
0
 def perform_destroy(self, instance):
     account_repository = accounts.AccountRepository()
     try:
         account_repository.delete(instance)
     except accounts.CantDeleteNonEmptyAccount:
         raise serializers.ValidationError("can't delete non-empty account")
Beispiel #20
0
 def perform_destroy(self, instance):
     account_repository = accounts.AccountRepository()
     account_repository.delete_event(instance)