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)
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", )
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, )
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, )
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, )
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 )
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." )
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']}" )
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
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)
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)
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)
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")
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
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")
def perform_destroy(self, instance): account_repository = accounts.AccountRepository() account_repository.delete_event(instance)