Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kraken Margin Trading #120

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,76 @@ Weitere Besonderheiten die sich so nicht im BMF-Schreiben wiederfinden, sind im
Bei Binance gibt es die Möglichkeit, andere Personen über einen Link zu werben.
Bei jeder Transaktion der Person erhält man einen kleinen Anteil derer Gebühren als Reward gutgeschrieben.
Die Einkünfte durch diese Rewards werden durch CoinTaxman als Einkünfte aus sonstigen Leistungen ausgewiesen und damit wie eine übliche Kunden-werben-Kunden-Prämie erfasst.

### 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
4 changes: 3 additions & 1 deletion src/balance_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def not_sold(self) -> decimal.Decimal:
"""
not_sold = self.op.change - self.sold
# If the left over amount is <= 0, this coin shouldn't be in the queue.
assert not_sold > 0, f"{not_sold=} should be > 0"
assert (
not_sold > 0
), f"{not_sold=} {self.op.coin} should be > 0 ({self.op.type_name})"
return not_sold


Expand Down
122 changes: 107 additions & 15 deletions src/book.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,12 +605,13 @@ 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",
"withdrawal": "Withdrawal",
"rollover": "MarginFee",
}

with open(file_path, encoding="utf8") as f:
Expand All @@ -622,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,
Expand Down Expand Up @@ -691,12 +708,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
Expand All @@ -713,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 "
Expand All @@ -730,7 +787,22 @@ 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", "MarginLoss"]:
if operation == "MarginFee":
assert (
change == 0
), "Margin fee operation should 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:
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
Expand All @@ -746,7 +818,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
Expand Down Expand Up @@ -842,6 +915,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
)
Expand All @@ -861,6 +935,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.

Expand Down Expand Up @@ -1309,6 +1386,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,
Expand Down Expand Up @@ -1390,6 +1468,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",
Expand Down Expand Up @@ -1677,9 +1768,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:
Expand Down
1 change: 1 addition & 0 deletions src/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ class Fiat(Enum):
"ZJPY": "JPY",
"ZUSD": "USD",
# Crypto:
"BTC": "XBT",
"XETC": "ETC",
"XETH": "ETH",
"XLTC": "LTC",
Expand Down
4 changes: 3 additions & 1 deletion src/price_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading