From 6371b455894294e1e113685b3029ac1cc22ae133 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Sun, 10 Apr 2022 13:10:20 +0200 Subject: [PATCH 01/13] ADD taxation for margin trading in readme - only my personal interpretation, no legal advice --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/README.md b/README.md index c5f39656..248ccf7e 100644 --- a/README.md +++ b/README.md @@ -208,3 +208,76 @@ Es ist also keine typische Kunden-werben-Kunden-Prämie sondern eher eine Kommis Für das Erste handhabe ich es wie eine Kunden-werben-Kunden-Prämie in Form eines Sachwerts. Sprich, die BTC werden zum Zeitpunkt des Erhalts in ihren EUR-Gegenwert umgerechnet und den Einkünften aus sonstigen Leistungen hinzugefügt. Aufgrund eines fehlenden steuerlichen Anschaffungsvorgangs ist eine Veräußerung steuerfrei. + +### Future- / Margin-Trading + +#### Unterscheidung Veräußerungsgeschäft / Termingeschäft + +> Gewinne aus Future-Trades stellen in der Regel Einkünfte aus Kapitalvermögen dar und unterliegen damit der Kapitalertragsteuer. Maßgebend für die steuerliche Beurteilung ist allerdings weniger die von der Börse gewählte Begrifflichkeit, sondern vielmehr die konkrete Ausgestaltung des angebotenen Finanzprodukts. Im Einzelfall kann deshalb unter Umständen auch bei Futures ein privates Veräußerungsgeschäft gemäß § 23 EStG vorliegen, das zu einer Besteuerung nach dem persönlichen Einkommensteuersatz führt. Im Kern kommt es für die Abgrenzung darauf an, ob das Geschäft wie beim Spot Trading auf die Lieferung einer Kryptowährung abzielt (dann ist § 23 EStG einschlägig) oder ob die Lieferung lediglich einen Differenzausgleich darstellt (dann liegen Kapitaleinkünfte gemäß § 23 Abs. 2 Satz. 1 Nr. 3 EStG vor). [...] +> +> Parallel dazu lassen sich die Überlegungen auf das Margin Trading übertragen, weshalb Gewinne aus Margin Trades immer nur dann unter Kapitaleinkünfte (§ 20 EStG) fallen, wenn keine Lieferung einer Kryptowährung, sondern ein Differenzausgleich durchgeführt wird. Kommt es hingegen zu einer Lieferung einer Kryptowährung, liegt ein privates Veräußerungsgeschäft gemäß § 23 Abs. 1 Satz 1 Nr. 2 EStG vor. +> +> [...] +> +> Stammen die erhaltenen Bitcoins aus einer Auszahlung resultierend aus einem Differenzausgleich, ist anschließend eine steuerfreie Veräußerung möglich. Da die Gewinne in Bitcoin bereits nach Maßgabe der Kapitalertragsteuer gemäß § 20 EStG versteuert wurden, greift auch keine Jahresfrist. +> +> [...] +> +> Solange die Position offen ist, muss diese auch nicht versteuert werden. Erst ab dem Zeitpunkt, in dem Investoren tatsächlich Einnahmen zugeflossen sind, findet die Besteuerung statt (sog. Zufluss-/Abflussprinzip). +> +> [...] +> +> Entstandene Gebühren fallen unter die sog. Werbungskosten. Da in den meisten Fällen beim Future Trading Kapitaleinkünfte vorliegen, sind die Werbungskosten bereits durch den Pauschbetrag in Höhe von 801 Euro (bzw. 1.602 Euro für verheiratete Paare) abgegolten. Die Gebühren können deshalb nicht gesondert steuerlich geltend gemacht werden, um die Steuerlast zu mindern. + +[Quelle](https://winheller.com/blog/besteuerung-future-margin-trading/) +[Wörtlich zitiert vom 18.02.2022] + +#### Fallbeispiel + +> Person A schließt mit Person B einen Vertrag, der A das Recht einräumt, in Zukunft einen BTC von B zu erhalten, der momentan 35.000 Euro wert ist. Wird der Kontrakt fällig und A erhält von B den BTC, liegt ein Fall von § 23 Abs. 1 Satz 1 Nr. 2 EStG vor. Das heißt für B, dass er die Veräußerung des BTC mit seinem persönlichen Einkommensteuersatz versteuern muss. Für A bedeutet das hingegen, dass eine Anschaffung vorliegt und damit die Jahresfrist gilt. +> +> Treffen A und B hingegen im oben geschilderten Fall die Vereinbarung, dass A am Ende des Vertrages die Wahl hat, ob er einen BTC bekommt oder alternativ den Gegenwert der Differenz zum aktuellen Kurs, dann liegt ein Termingeschäft gem. § 20 Abs. 2 Satz 1 Nr. 3 EStG vor. Das gilt auch dann, wenn die Differenz in BTC gezahlt wird. Steigt der Kurs von BTC am Ende des Kontrakts auf 37.000 Euro an, erhält A den Gegenwert der Differenz (37.000 Euro – 35.000 Euro = 2.000 Euro) in BTC. Der Gewinn muss von A pauschal mit 25 Prozent Kapitalertragsteuer versteuert werden, die Jahresfrist aus § 23 EStG greift hingegen nicht. Gegebenenfalls liegt bei B ein privates Veräußerungsgeschäft i.S.v. § 23 EStG vor. + +[Quelle](https://hub.accointing.com/crypto-tax-regulations/germany/tax-break-germany-derivate-und-futures-winheller) +[Wörtlich zitiert vom 18.02.2022] + +#### Werbungskosten für Termingeschäfte + +> Ab dem VZ 2009 ist als Werbungskosten ein Betrag von 801 € bzw. 1 602 € bei Zusammenveranlagung abzuziehen (Sparer-Pauschbetrag, § 20 Abs. 9 EStG); der Abzug der tatsächlichen Werbungskosten ist ausgeschlossen. Die früheren Regelungen zum Werbungskosten Pauschbetrag und Sparer-Freibetrag wurden mit Einführung der Abgeltungsteuer aufgehoben. +> +> [...] +> +> In folgenden Fällen sind die Kosten auch ab 2009 weiterhin abzugsfähig: +> - Veräußerungskosten und Kosten in Zusammenhang mit Termingeschäften werden bei der Veräußerungsgewinnermittlung nach § 20 Abs. 4 EStG berücksichtigt. +> +> [...] +> + +[Quelle](https://datenbank.nwb.de/Dokument/97088/) +[Wörtlich zitiert vom 18.02.2022] + +#### Verrechnung von Verlusten aus Termingeschäften + +> Während es vor dem 01.01.2021 möglich war, Verluste aus Termingeschäften uneingeschränkt mit den Einkünften aus Kapitalvermögen zu verrechnen, ist dies aufgrund des neu eingeführten § 20 Abs. 6 Satz 5 EStG seit 2021 nicht mehr ohne Weiteres möglich: +> 1. Verluste dürfen nur noch mit Gewinnen aus Termingeschäften und mit Erträgen aus Stillhaltergeschäften verrechnet werden. +> 2. Außerdem ist die Verlustverrechnung auf 20.000 Euro jährlich begrenzt. +> +> Zwar können die nicht verrechneten Verluste in die Folgejahre vorgetragen werden. Aber auch dann ist eine Verlustverrechnung der Höhe nach auf 20.000 Euro pro Jahr begrenzt. Das führt faktisch zu einer Mindestbesteuerung von Gewinnen. + +[Quelle](https://www.winheller.com/bankrecht-finanzrecht/bitcointrading/bitcoinundsteuer/verlustverrechnung.html) +[Wörtlich zitiert vom 18.02.2022] + +#### Zusammenfassung + +Zusammenfassung der Besteuerung des Margin-Tradings in meinen Worten: +- Gewinne/Verluste werden besteuert, sobald die Margin-Positionen ausgeglichen bzw. geschlossen werden +- Wird eine Margin-Position ausgeglichen ("settled"), d.h. die Kryptowährung wird zu Vertragsende zum Startpreis ge-/verkauft, liegt ein privates Veräußerungsgeschäft vor + - Für private Veräußerungsgeschäfte gelten die oben angeführten Regeln, inklusive der einjährigen Haltefrist +- Wird eine Margin-Position geschlossen ("closed", Differenzausgleich), liegt ein Termingeschäft vor + - Die Gewinne bzw. Verluste fallen unter Kapitaleinkünfte (§ 20 EStG) + - Erhaltene Kryptowährung aus Differenzausgleichen kann steuerfrei veräußert werden + - Es gibt keine einjährige Haltefrist + - Gebühren können nur abgezogen werden, wenn der Freibetrag von 801 / 1602 Euro bereits ausgeschöpft wird + - Die Verlustrechnung ist auf 20.000 Euro jährlich begrenzt und darf nicht mit Gewinnen aus privaten Veräußerungsgeschäften verrechnet werden +- Steht es dem Investor bis zum Ende offen, ob eine Margin-Position ausgeglichen ("settled") oder geschlossen ("closed") werden kann, liegt automatisch ein Termingeschäft vor und es gelten die gleichen Regelungen wie für geschlossene Positionen. Dies trifft für folgende Börsen zu: + - Kraken \ No newline at end of file From 4df06c6e58f7e0c71405768521c949376ca187e1 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Sun, 10 Apr 2022 13:15:03 +0200 Subject: [PATCH 02/13] ADD initial margin trading for Kraken --- src/balance_queue.py | 2 +- src/book.py | 44 ++++++++++++++++++++++++++++++++++++-------- src/config.py | 1 - src/taxman.py | 28 ++++++++++++++++++++++++++++ src/transaction.py | 12 ++++++++++++ 5 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/balance_queue.py b/src/balance_queue.py index a4b005bd..3f03369f 100644 --- a/src/balance_queue.py +++ b/src/balance_queue.py @@ -114,7 +114,7 @@ def sell( # Calculate the amount of coins, which are not sold yet. not_sold = bop.op.change - bop.sold - assert not_sold > 0 + assert not_sold > 0, f"{not_sold} {bop.op.coin} not sold ({type(bop.op)})" if not_sold > change: # There are more coins left than change. diff --git a/src/book.py b/src/book.py index 98879b07..4198cda2 100644 --- a/src/book.py +++ b/src/book.py @@ -502,6 +502,7 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: "staking": "StakingInterest", "deposit": "Deposit", "withdrawal": "Withdrawal", + "rollover": "MarginFee", } with open(file_path, encoding="utf8") as f: @@ -562,12 +563,24 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: if operation is None: if _type == "trade": operation = "Sell" if change < 0 else "Buy" - elif _type in ["margin trade", "rollover", "settled", "margin"]: - log.error( - f"{file_path} row {row}: Margin trading is currently not " - "supported. Please create an Issue or PR." - ) - raise RuntimeError + elif _type == "margin": + # Margin positions for Kraken always fall under income from + # capital, as the user can decide until the end if it is closed + # or settled. "rollover" entries contain margin fees between + # start and end. The start of a margin position is denoted with + # a "margin" entry with zero change. For closed positions, the + # end is marked with a "margin" entry containing the net + # gain/loss of the position. + # if the change is zero, consider only the fees + if change == 0: + operation = "MarginFee" + elif change > 0: + operation = "MarginGain" + else: + operation = "MarginLoss" + elif _type == "settled": + # "settled" entries mark the end of settled positions + operation = "MarginGain" if change > 0 else "MarginLoss" elif _type == "transfer": if num_columns == 9: # for backwards compatibility assume Airdrop for staking @@ -601,7 +614,20 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: # Validate data. assert operation assert coin - assert change + + # Margin trading: Add operations and fees to list + if operation in ["MarginFee", "MarginGain", "MarginSell"]: + if operation == "MarginFee": + assert change == 0, "Margin fee should be only contain fee" + if change: + # add margin gain/losses to operation list + self.append_operation( + operation, utc_time, platform, change, coin, row, file_path + ) + if fee != 0: + self.append_operation( + "MarginFee", utc_time, platform, fee, coin, row, file_path + ) # Skip duplicate entries for deposits / withdrawals and additional # deposit / withdrawal lines for staking / unstaking / staking reward @@ -617,7 +643,8 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: # == None: Initial value (first occurrence) # == False: No operation has been appended (second occurrence) # == True: Operation has already been appended, this should not happen - if operation in ["Deposit", "Withdrawal"]: + elif operation in ["Deposit", "Withdrawal"]: + assert change # First, create the operations op = self.create_operation( operation, utc_time, platform, change, coin, row, file_path @@ -700,6 +727,7 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: # for all other operation types else: + assert change self.append_operation( operation, utc_time, platform, change, coin, row, file_path ) diff --git a/src/config.py b/src/config.py index 567fd24a..7389636a 100644 --- a/src/config.py +++ b/src/config.py @@ -60,7 +60,6 @@ def IS_LONG_TERM(buy: datetime, sell: datetime) -> bool: return buy + relativedelta(years=1) < sell - else: raise NotImplementedError(f"Your country {COUNTRY} is not supported.") diff --git a/src/taxman.py b/src/taxman.py index edf26f02..4e1b03a7 100644 --- a/src/taxman.py +++ b/src/taxman.py @@ -128,6 +128,7 @@ def evaluate_sell( transaction.CoinLendInterest, transaction.StakingInterest, transaction.Commission, + transaction.MarginGain, ), ) and not sc.op.coin == config.FIAT @@ -176,6 +177,33 @@ def evaluate_sell( elif isinstance(op, transaction.Sell): if tx_ := evaluate_sell(op): self.tax_events.append(tx_) + elif isinstance(op, transaction.MarginFee): + balance.remove_fee(op.change) + if self.in_tax_year(op): + # Fees reduce taxed gain. + taxation_type = ( + "Kapitaleinkünfte aus Margin Trading (Werbungskosten)" + ) + taxed_gain = -self.price_data.get_cost(op) + tx = transaction.TaxEvent(taxation_type, taxed_gain, op) + self.tax_events.append(tx) + elif isinstance(op, transaction.MarginGain): + balance.put(op) + if self.in_tax_year(op): + taxation_type = "Kapitaleinkünfte aus Margin Trading" + taxed_gain = self.price_data.get_cost(op) + tx = transaction.TaxEvent(taxation_type, taxed_gain, op) + self.tax_events.append(tx) + elif isinstance(op, transaction.MarginLoss): + # First, sell the lost coin to evaluate gain/loss from holding it + if tx_ := evaluate_sell(op): + self.tax_events.append(tx_) + # Then, add the total loss to the income from capital + if self.in_tax_year(op): + taxation_type = "Kapitaleinkünfte aus Margin Trading" + taxed_gain = -self.price_data.get_cost(op) + tx = transaction.TaxEvent(taxation_type, taxed_gain, op) + self.tax_events.append(tx) elif isinstance( op, (transaction.CoinLendInterest, transaction.StakingInterest) ): diff --git a/src/transaction.py b/src/transaction.py index 1c3c4fbf..11c4c2ed 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -99,6 +99,18 @@ class Sell(Transaction): pass +class MarginFee(Transaction): + pass + + +class MarginGain(Transaction): + pass + + +class MarginLoss(Transaction): + pass + + class CoinLendInterest(Transaction): pass From c5874560b3a0fd562d053d593bad3c4a05ab65d7 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Sun, 1 May 2022 12:31:43 +0200 Subject: [PATCH 03/13] CHANGE no "virtual" sell of coins that cause margin losses --- src/taxman.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/taxman.py b/src/taxman.py index 4e1b03a7..827cda7e 100644 --- a/src/taxman.py +++ b/src/taxman.py @@ -196,8 +196,8 @@ def evaluate_sell( self.tax_events.append(tx) elif isinstance(op, transaction.MarginLoss): # First, sell the lost coin to evaluate gain/loss from holding it - if tx_ := evaluate_sell(op): - self.tax_events.append(tx_) + #if tx_ := evaluate_sell(op): + # self.tax_events.append(tx_) # Then, add the total loss to the income from capital if self.in_tax_year(op): taxation_type = "Kapitaleinkünfte aus Margin Trading" From 1c1622e59df17b96ea27c6491d7e8546aaa36a7d Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Thu, 26 May 2022 16:36:53 +0200 Subject: [PATCH 04/13] FIX margin losses, ADD comments for clarification --- src/book.py | 2 +- src/taxman.py | 11 +++++------ src/transaction.py | 8 ++++++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/book.py b/src/book.py index 4804c4bf..3d7a2385 100644 --- a/src/book.py +++ b/src/book.py @@ -651,7 +651,7 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: assert coin # Margin trading: Add operations and fees to list - if operation in ["MarginFee", "MarginGain", "MarginSell"]: + if operation in ["MarginFee", "MarginGain", "MarginLoss"]: if operation == "MarginFee": assert change == 0, "Margin fee should be only contain fee" if change: diff --git a/src/taxman.py b/src/taxman.py index 7ae93dee..b26fccfc 100644 --- a/src/taxman.py +++ b/src/taxman.py @@ -476,9 +476,9 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: self.tax_report_entries.append(report_entry) elif isinstance(op, tr.MarginFee): - self.remove_fees_from_balance(op.fees) + # Fees for margin trading + self.remove_from_balance(op) if in_tax_year(op): - # Fees reduce taxed gain. taxation_type = "Kapitaleinkünfte aus Margin Trading" report_entry = tr.MarginReportEntry( platform=op.platform, @@ -492,6 +492,7 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: self.tax_report_entries.append(report_entry) elif isinstance(op, tr.MarginGain): + # Gains from margin trading self.add_to_balance(op) if in_tax_year(op): taxation_type = "Kapitaleinkünfte aus Margin Trading" @@ -507,10 +508,8 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: self.tax_report_entries.append(report_entry) elif isinstance(op, tr.MarginLoss): - # First, sell the lost coin to evaluate gain/loss from holding it - # if tx_ := evaluate_sell(op): - # self.tax_events.append(tx_) - # Then, add the total loss to the income from capital + # Losses from margin trading + self.remove_from_balance(op) if in_tax_year(op): taxation_type = "Kapitaleinkünfte aus Margin Trading" report_entry = tr.MarginReportEntry( diff --git a/src/transaction.py b/src/transaction.py index 15668d86..fee2ea36 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -180,14 +180,22 @@ class Sell(Transaction): class MarginFee(Transaction): + """Fees for margin trading""" + pass class MarginGain(Transaction): + """Gains from margin trading. + This is already a taxable value, no buy/sell calculation required.""" + pass class MarginLoss(Transaction): + """"Losses from margin trading. + This is already a taxable value, no buy/sell calculation required.""" + pass From 82dc726ca7fe72acd8731c8712adaee1c5376803 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Thu, 26 May 2022 16:38:34 +0200 Subject: [PATCH 05/13] UPDATE verbosity of warnings --- src/balance_queue.py | 2 +- src/book.py | 7 ++++--- src/taxman.py | 4 +++- src/transaction.py | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/balance_queue.py b/src/balance_queue.py index 45126c79..c7f0f32f 100644 --- a/src/balance_queue.py +++ b/src/balance_queue.py @@ -43,7 +43,7 @@ def not_sold(self) -> decimal.Decimal: # If the left over amount is <= 0, this coin shouldn't be in the queue. assert ( not_sold > 0 - ), f"{not_sold=} {self.op.coin} should be > 0 ({type(self.op)})" + ), f"{not_sold=} {self.op.coin} should be > 0 ({self.op.type_name})" return not_sold diff --git a/src/book.py b/src/book.py index 3d7a2385..df62f9f6 100644 --- a/src/book.py +++ b/src/book.py @@ -1604,9 +1604,10 @@ def match_fees(self) -> None: log.warning( "Fee matching is not implemented for this case. " "Your fees will be discarded and are not evaluated in " - "the tax evaluation.\n" - "Please create an Issue or PR.\n\n" - f"{matching_operations=}\n{fees=}" + "the tax evaluation. " + "Please create an Issue or PR. " + f"Found {len(matching_operations)} matching operations:\n" + f"{matching_operations=}\n{fees=}\n" ) def resolve_trades(self) -> None: diff --git a/src/taxman.py b/src/taxman.py index b26fccfc..108559d6 100644 --- a/src/taxman.py +++ b/src/taxman.py @@ -170,7 +170,9 @@ def get_buy_cost(self, sc: tr.SoldCoin) -> decimal.Decimal: "previously sold coins of the trade. " "The calculated buy cost might be wrong. " "This may lead to a false tax evaluation.\n" - f"{sc.op}" + f"{sc.op.type_name} {sc.op.change} {sc.op.coin} @ " + f"{sc.op.platform} {sc.op.utc_time}, " + f"row(s) {sc.op.line} of {sc.op.file_path.name}" ) buy_value = self.price_data.get_cost(sc) else: diff --git a/src/transaction.py b/src/transaction.py index fee2ea36..04c129b8 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -193,7 +193,7 @@ class MarginGain(Transaction): class MarginLoss(Transaction): - """"Losses from margin trading. + """Losses from margin trading. This is already a taxable value, no buy/sell calculation required.""" pass From c8eec27712574b5769e122495f105c3ec60eeb90 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Thu, 26 May 2022 17:14:07 +0200 Subject: [PATCH 06/13] FIX make install-dev --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 59d2899e..bca22f1c 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ install: pip install -r requirements.txt install-dev: install - pip -r requirements-dev.txt + pip install -r requirements-dev.txt # Setup virtual environment venv: From f68171ca96db38cdcd24e5917b1f4cf571ab37c9 Mon Sep 17 00:00:00 2001 From: Jeppy Date: Sat, 4 Jun 2022 10:48:22 +0200 Subject: [PATCH 07/13] UPDATE format --- src/book.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/book.py b/src/book.py index df62f9f6..a8bb2630 100644 --- a/src/book.py +++ b/src/book.py @@ -606,7 +606,7 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: # a "margin" entry with zero change. For closed positions, the # end is marked with a "margin" entry containing the net # gain/loss of the position. - # if the change is zero, consider only the fees + # If the change is zero, consider only the fees. if change == 0: operation = "MarginFee" elif change > 0: @@ -614,7 +614,7 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: else: operation = "MarginLoss" elif _type == "settled": - # "settled" entries mark the end of settled positions + # "settled" entries mark the end of settled positions. operation = "MarginGain" if change > 0 else "MarginLoss" elif _type == "transfer": if num_columns == 9: @@ -650,16 +650,16 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: assert operation assert coin - # Margin trading: Add operations and fees to list + # Margin trading: Add operations and fees to list. if operation in ["MarginFee", "MarginGain", "MarginLoss"]: if operation == "MarginFee": assert change == 0, "Margin fee should be only contain fee" if change: - # add margin gain/losses to operation list + # Add margin gain/losses to operation list. self.append_operation( operation, utc_time, platform, change, coin, row, file_path ) - if fee != 0: + if fee: self.append_operation( "MarginFee", utc_time, platform, fee, coin, row, file_path ) From 2ce428aa1ff905d22f612dc1969b4922e2466e18 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Mon, 24 Oct 2022 13:43:41 +0200 Subject: [PATCH 08/13] UPDATE margin reporting according to review comments --- src/book.py | 4 +++- src/taxman.py | 31 +++++++++++++++++------- src/transaction.py | 59 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/book.py b/src/book.py index 30fca1a0..52d8c7b1 100644 --- a/src/book.py +++ b/src/book.py @@ -654,7 +654,9 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: # Margin trading: Add operations and fees to list. if operation in ["MarginFee", "MarginGain", "MarginLoss"]: if operation == "MarginFee": - assert change == 0, "Margin fee should be only contain fee" + assert ( + change == 0 + ), "Margin fee operation should only contain fee." if change: # Add margin gain/losses to operation list. self.append_operation( diff --git a/src/taxman.py b/src/taxman.py index cc09717f..4384bfbf 100644 --- a/src/taxman.py +++ b/src/taxman.py @@ -481,15 +481,20 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: # Fees for margin trading self.remove_from_balance(op) if in_tax_year(op): - taxation_type = "Kapitaleinkünfte aus Margin Trading" + taxation_type = "Kapitaleinkünfte" + if not op.remark: + _remark = "Margin Fee" + else: + _remark = "Margin Fee, " + op.remark + assert op.change > 0, "Change should be positive." report_entry = tr.MarginReportEntry( platform=op.platform, - amount=op.change, + amount=-op.change, coin=op.coin, utc_time=op.utc_time, interest_in_fiat=-self.price_data.get_cost(op), taxation_type=taxation_type, - remark=op.remark, + remark=_remark, ) self.tax_report_entries.append(report_entry) @@ -497,7 +502,12 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: # Gains from margin trading self.add_to_balance(op) if in_tax_year(op): - taxation_type = "Kapitaleinkünfte aus Margin Trading" + taxation_type = "Kapitaleinkünfte" + if not op.remark: + _remark = "Margin Gain" + else: + _remark = "Margin Gain, " + op.remark + assert op.change > 0, "Change should be positive." report_entry = tr.MarginReportEntry( platform=op.platform, amount=op.change, @@ -505,7 +515,7 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: utc_time=op.utc_time, interest_in_fiat=self.price_data.get_cost(op), taxation_type=taxation_type, - remark=op.remark, + remark=_remark, ) self.tax_report_entries.append(report_entry) @@ -513,15 +523,20 @@ def _evaluate_taxation_GERMANY(self, op: tr.Operation) -> None: # Losses from margin trading self.remove_from_balance(op) if in_tax_year(op): - taxation_type = "Kapitaleinkünfte aus Margin Trading" + taxation_type = "Kapitaleinkünfte" + if not op.remark: + _remark = "Margin Loss" + else: + _remark = "Margin Loss, " + op.remark + assert op.change > 0, "Change should be positive." report_entry = tr.MarginReportEntry( platform=op.platform, - amount=op.change, + amount=-op.change, coin=op.coin, utc_time=op.utc_time, interest_in_fiat=-self.price_data.get_cost(op), taxation_type=taxation_type, - remark=op.remark, + remark=_remark, ) self.tax_report_entries.append(report_entry) diff --git a/src/transaction.py b/src/transaction.py index 04c129b8..e01044fd 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -702,6 +702,60 @@ def _labels(cls) -> list[str]: ] +class MarginReportEntry(TaxReportEntry): + event_type = "Margin-Trading" + + def __init__( + self, + platform: str, + amount: decimal.Decimal, + utc_time: datetime.datetime, + coin: str, + interest_in_fiat: decimal.Decimal, + taxation_type: str, + remark: str, + ) -> None: + super().__init__( + first_platform=platform, + amount=amount, + first_utc_time=utc_time, + coin=coin, + first_value_in_fiat=interest_in_fiat, + is_taxable=True, + taxation_type=taxation_type, + remark=remark, + ) + + @classmethod + def _labels(cls) -> list[str]: + return [ + "Börse", + "-", + # + "Anzahl", + "Währung", + # + "Erhalten oder ausgegeben am", + "-", + # + "-", + "-", + "-", + "-", + "-", + "-", + # + "-", + "-", + "-", + # + "Gewinn/Verlust in EUR", + "davon steuerbar in EUR", + "Einkunftsart", + "Bemerkung", + ] + + class InterestReportEntry(TaxReportEntry): event_type = "Zinsen" @@ -764,10 +818,6 @@ class StakingInterestReportEntry(InterestReportEntry): event_type = "Staking Einkünfte" -class MarginReportEntry(InterestReportEntry): - event_type = "Margin-Trading" - - class AirdropReportEntry(TaxReportEntry): event_type = "Airdrops" @@ -962,6 +1012,7 @@ def __init__( tax_report_entry_order = [ BuyReportEntry, SellReportEntry, + MarginReportEntry, LendingInterestReportEntry, StakingInterestReportEntry, InterestReportEntry, From d5c2f5b3752376bdf715e7d8b17eaa035d1a867e Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:02:40 +0200 Subject: [PATCH 09/13] ADD support new Kraken API "invalid asset pair" error --- src/price_data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/price_data.py b/src/price_data.py index 59b239e0..444dc2c6 100644 --- a/src/price_data.py +++ b/src/price_data.py @@ -468,7 +468,9 @@ def _get_price_kraken( if not data["error"]: break - elif data["error"] == ["EGeneral:Invalid arguments"]: + elif data["error"] == ["EGeneral:Invalid arguments"] or data[ + "error" + ] == ["EQuery:Unknown asset pair"]: # add pair to invalid pairs list # leads to inversion of pair next time log.warning( From a3d980bf9aa6078ed2a8e2e14d3f06c12c19472e Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Sat, 17 Dec 2022 12:10:02 +0100 Subject: [PATCH 10/13] black auto format --- src/price_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/price_data.py b/src/price_data.py index 444dc2c6..923b143e 100644 --- a/src/price_data.py +++ b/src/price_data.py @@ -468,9 +468,9 @@ def _get_price_kraken( if not data["error"]: break - elif data["error"] == ["EGeneral:Invalid arguments"] or data[ - "error" - ] == ["EQuery:Unknown asset pair"]: + elif (data["error"] == ["EGeneral:Invalid arguments"]) or ( + data["error"] == ["EQuery:Unknown asset pair"] + ): # add pair to invalid pairs list # leads to inversion of pair next time log.warning( From 013f7b2a1ec569381835d52d0ab8f5a72c4f8a7a Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Sat, 3 Aug 2024 12:58:36 +0200 Subject: [PATCH 11/13] ADD Kraken BTC to XBT mapping --- src/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core.py b/src/core.py index 40644145..3f02e7ec 100644 --- a/src/core.py +++ b/src/core.py @@ -214,6 +214,7 @@ class Fiat(Enum): "ZJPY": "JPY", "ZUSD": "USD", # Crypto: + "BTC": "XBT", "XETC": "ETC", "XETH": "ETH", "XLTC": "LTC", From 6b6c5b44f1bfff377fd96b1f31b32d9bd17a772b Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:00:57 +0100 Subject: [PATCH 12/13] ADD support for new Kraken CSV format --- src/book.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/book.py b/src/book.py index 831628df..9da21896 100644 --- a/src/book.py +++ b/src/book.py @@ -623,8 +623,24 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: for columns in reader: num_columns = len(columns) - # Kraken ledgers export format from October 2020 and ongoing - if num_columns == 10: + # Kraken ledgers export format from 2025 and ongoing + if num_columns == 11: + ( + txid, + refid, + _utc_time, + _type, + subtype, + aclass, + _asset, + _wallet, + _amount, + _fee, + balance, + ) = columns + + # Kraken ledgers export format from October 2020 until 2025 + elif num_columns == 10: ( txid, refid, @@ -891,6 +907,9 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: def _read_kraken_ledgers_old(self, file_path: Path) -> None: self._read_kraken_ledgers(file_path) + def _read_kraken_ledgers_v2(self, file_path: Path) -> None: + self._read_kraken_ledgers(file_path) + def _read_bitpanda_pro_trades(self, file_path: Path) -> None: """Reads a trade statement from Bitpanda Pro. @@ -1339,6 +1358,7 @@ def detect_exchange(self, file_path: Path) -> Optional[str]: "coinbase_pro": 1, "kraken_ledgers_old": 1, "kraken_ledgers": 1, + "kraken_ledgers_v2": 1, "kraken_trades": 1, "bitpanda_pro_trades": 4, "bitpanda": 7, @@ -1420,6 +1440,19 @@ def detect_exchange(self, file_path: Path) -> Optional[str]: "fee", "balance", ], + "kraken_ledgers_v2": [ + "txid", + "refid", + "time", + "type", + "subtype", + "aclass", + "asset", + "wallet", + "amount", + "fee", + "balance", + ], "kraken_trades": [ "txid", "ordertxid", From ee16bd52d6dd85ea091cee824dda11955ce7fbc1 Mon Sep 17 00:00:00 2001 From: Griffsano <18743559+Griffsano@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:01:51 +0100 Subject: [PATCH 13/13] ADD support for new Kraken operation types --- src/book.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/book.py b/src/book.py index 9da21896..771146f4 100644 --- a/src/book.py +++ b/src/book.py @@ -605,8 +605,8 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: platform = "kraken" operation_mapping = { - "spend": "Sell", # Sell ordered via 'Buy Crypto' button - "receive": "Buy", # Buy ordered via 'Buy Crypto' button + "spend": "Sell", # Sell ordered via 'Buy Crypto' or 'Dust Sweeping' + "receive": "Buy", # Buy ordered via 'Buy Crypto' or 'Dust Sweeping' "reward": "StakingInterest", "staking": "StakingInterest", "deposit": "Deposit", @@ -742,6 +742,34 @@ def _read_kraken_ledgers(self, file_path: Path) -> None: elif subtype in ["spottostaking", "spotfromstaking"]: # duplicate entries for staking actions continue + elif subtype in ["spottofutures", "spotfromfutures"]: + # transfer between spot and futures + continue + else: + log.error( + f"{file_path} row {row}: Order subtype '{subtype}' is " + "currently not supported. Please create an Issue or PR." + ) + raise RuntimeError + elif _type == "earn": + if subtype == "reward": + operation = "StakingInterest" + elif subtype == "migration": + # Migration of "x.S" legacy staking balance to new staking + # infrastructure in "earn / bonded" wallet + continue + elif subtype == "allocation": + if change > 0: + operation = "Staking" + else: + # duplicate entries for staking actions + continue + elif subtype == "deallocation": + if change > 0: + operation = "StakingEnd" + else: + # duplicate entries for staking actions + continue else: log.error( f"{file_path} row {row}: Order subtype '{subtype}' is "