Skip to content

Commit 46b8abe

Browse files
committed
Fixes in bitpanda price fetching; more special case handling
* bitpanda general: Bitpanda Pro, whose API we use to fetch prices for bitpanda transactions, is now One Trading. As a result, their API is available under a different address and with a slightly different response. This commit contains the fixes for both, the address and the parsing/usage of the result. * bitpanda general: Since One Trading doesn't offer the same coins as Bitpanda anymore, some coin/fiat pairs aren't available there (like BEST/EUR, ETHW/EUR, LTC/EUR and others). To differentiate between a "market" not being available and other errors, we raise a ValueError in case the "market" is not available. I chose ValueError because catching LookupError also catches errors with indices in lists, which should be thrown. * bitpanda general: The ValueError is caught in price_data.py in the `get_cost` method. In the last commit, I added a new field `exported_price` to the `Operation` data class, which is used in the exception handling to use the price from the csv export (it's better than nothing). For BEST, there is sadly no price available if the BEST transaction is a Withdrawal (Fee). For now, we assume a value of 0 in that case (I don't know how to fix this otherwise). * bitpanda LUNC airdrop: I added special handling for the LUNC airdrop that happened in May 2022 because of a blockchain crash and subsequent fork. Sadly, the price can't be retrieved using the API and the airdrop didn't have a price associated. CoinTaxman should throw an exception because it can't fetch a price and it should also say which line the airdrop is in. I used that to edit my csv export and input a ridiculously small price since the price was really low anyway. * bitpanda staking rewards: Sometime before 2022/6/14, bitpanda used "transfer" for staking rewards. Incoming crypto "transfers" before that date, that aren't BEST, are therefore classified as (staking-)reward.
1 parent 5eca87e commit 46b8abe

4 files changed

Lines changed: 85 additions & 25 deletions

File tree

config.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ LOG_LEVEL = DEBUG
2828
# If False, all airdrops will be taxed as `Einkünfte aus sonstigen Leistungen`.
2929
# Setting this config falsly will result in a wrong tax calculation.
3030
# Please inform yourself and help to resolve this issue by working on/with #115.
31-
# Some airdrops can be classified as gifts (Schenkung) or income (Eink├╝nfte)
31+
# Some airdrops can be classified as gifts (Schenkung) or income (Einkünfte)
3232
# relatively savely. For those, there is a flag to signal either type.
3333
# See the AirdropGift and AirdropIncome classes in transaction.py and their usage in book.py
3434
ALL_AIRDROPS_ARE_GIFTS = True

src/book.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ def _read_bitpanda(self, file_path: Path) -> None:
10991099
# CocaCola transfer, which I don't want to track. Would need to
11001100
# be implemented if need be.
11011101
if operation == "transfer":
1102-
if asset == "BEST" and asset_class == "Cryptocurrency":
1102+
if asset == "BEST" and asset_class == "Cryptocurrency" and inout == "incoming":
11031103
# BEST is awarded for trading activity and holding a portfolio at bitpanda
11041104
# The BEST awards are listed as "transfer" but must be processed as Airdrop (non-taxable)
11051105
operation = "airdrop_gift"
@@ -1132,6 +1132,48 @@ def _read_bitpanda(self, file_path: Path) -> None:
11321132
f"In row {row} in file {file_path}."
11331133
)
11341134
operation = "airdrop_gift"
1135+
elif (
1136+
inout == "incoming"
1137+
and asset == "LUNC"
1138+
and asset_class == "Fiat"
1139+
and utc_time.year == 2022
1140+
and utc_time.month == 5
1141+
):
1142+
# In May 2022 the Terra (LUNA) blockchain crashed. In response, a new chain
1143+
# Terra 2.0 (LUNA) was created. The new old chain is still tradeable as
1144+
# Terra Classic (LUNC) and holders of LUNA before the crash received their
1145+
# LUNC tokens as airdrop. This also applied to LUNA tokens held through
1146+
# bitpanda crypto indices.
1147+
# Source for bitpanda LUNC airdrop:
1148+
# https://support.bitpanda.com/hc/en-us/articles/4995318011292-Terra-2-0-LUNA-Airdrop
1149+
#
1150+
# The German law regarding this case is not entirely clear:
1151+
# https://www.winheller.com/bankrecht-finanzrecht/bitcointrading/bitcoinundsteuer/besteuerung-hardforks-ledger-splits.html
1152+
# TODO: This should actually copy the history from the original LUNA history.
1153+
log.warning(
1154+
f"WARNING: Airdrop of {asset} is a result of the fork of the "
1155+
f"LUNA blockchain in May 2022. The legal status of "
1156+
f"taxation of hardfork results is not clear in German law. "
1157+
f"Also, the date of procurement should be set to the date(s) "
1158+
f"of procurement of the original coins, essentially copying "
1159+
f"the history of the original chain, which is NOT YET IMPLEMENTED. "
1160+
f"See https://support.bitpanda.com/hc/en-us/articles/4995318011292-Terra-2-0-LUNA-Airdrop "
1161+
f"for more information. "
1162+
f"Please open an issue or PR if you know how to resolve this. "
1163+
f"In row {row} in file {file_path}."
1164+
)
1165+
# Rewrite this asset_class because "Fiat" clearly wrong.
1166+
asset_class = "Cryptocurrency"
1167+
operation = "airdrop_gift"
1168+
elif (
1169+
inout == "incoming"
1170+
and asset_class == "Cryptocurrency"
1171+
and asset != "BEST"
1172+
and utc_time < datetime.datetime(2022, 6, 14, 0, 0, 0, 0, utc_time.tzinfo)
1173+
):
1174+
# Bitpanda tagged incoming staking rewards as incoming transfer until June 14 2022
1175+
# or a few days before that date. After that, staking rewards are correctly tagged as "reward".
1176+
operation = "reward"
11351177
else:
11361178
log.warning(
11371179
f"'Transfer' operations are not "

src/price_data.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,10 @@ def _get_price_bitpanda_pro(
355355
)
356356
r = requests.get(baseurl, params=params)
357357

358-
assert r.status_code == 200, f"No valid response from ONE TRADING (ex Bitpanda Pro) API\nError: {r.json()['error']}"
359358
data = r.json()
359+
if r.status_code == 400 and data["error"] == f"The requested market {base_asset}_{quote_asset} is not available.":
360+
raise ValueError(data["error"])
361+
assert r.status_code == 200, f"No valid response from ONE TRADING (ex Bitpanda Pro) API\nError: {r.json()['error']}"
360362

361363
# exit loop if data is valid
362364
if data:
@@ -380,11 +382,12 @@ def _get_price_bitpanda_pro(
380382
raise RuntimeError
381383

382384
# this should never be triggered, but just in case assert received data
383-
assert data, f"No valid price data for {base_asset} / {quote_asset} at {end}"
385+
assert data["candlesticks"], f"No valid price data for {base_asset} / {quote_asset} at {end}"
386+
data = data["candlesticks"]
384387

385-
# simply take the average of the latest data element
386-
high = misc.force_decimal(data[-1]["high"])
387-
low = misc.force_decimal(data[-1]["low"])
388+
# simply take the average of the first data element
389+
high = misc.force_decimal(data[0]["high"])
390+
low = misc.force_decimal(data[0]["low"])
388391

389392
# if spread is greater than 3%
390393
if (high - low) / high > 0.03:
@@ -607,24 +610,39 @@ def get_cost(
607610
reference_coin: str = config.FIAT,
608611
) -> decimal.Decimal:
609612
op = op_sc if isinstance(op_sc, tr.Operation) else op_sc.op
610-
if op.coin in ["ETHW", "BEST"] and op.platform == "bitpanda":
611-
# ETHW and BEST are not available via ONE TRADING (ex Bitpanda Pro) API
612-
# => use the price from the exported data.
613-
if op.exported_price is not None:
614-
return op.exported_price
615-
if op.coin == "BEST" and isinstance(op, tr.Fee):
613+
try:
614+
price = self.get_price(op.platform, op.coin, op.utc_time, reference_coin)
615+
except ValueError as e:
616+
log.warning(
617+
f"The API didn't provide a valid response. Using the price from the csv file if possible.\n"
618+
f"\t\tCoin: {op.coin} | Op: {type(op).__name__} | Platform: {op.platform} | Row: {op.line} | File: {op.file_path}\n"
619+
f"\t\tCaught exception: {e}"
620+
)
621+
if op.platform == "bitpanda":
622+
# LUNC, ETHW, BEST and maybe more are not available via ONE TRADING (ex Bitpanda Pro) API
623+
# => use the price from the exported data.
624+
if op.exported_price is not None:
625+
price = op.exported_price
626+
616627
# Fees paid with BEST don't have a value given in the exported data.
617628
# The value also can't be queried from the ONE TRADING (ex Bitpanda Pro) API (anymore)
629+
if op.coin == "BEST" and isinstance(op, tr.Fee):
630+
log.warning(
631+
f"Can't get price for '{type(op).__name__}' of {op.coin} on platform {op.platform} anymore.\n"
632+
f"A withdrawal of BEST on bitpanda is likely a deduction of fees. For now we'll assume a value of 0.\n"
633+
f"For accurately calculating fees, this needs to be fixed. PRs welcome!\n"
634+
f"(row {op.line} in {op.file_path}"
635+
)
636+
return 0
637+
else:
618638
log.warning(
619-
f"Can't get price for '{type(op).__name__}' of {op.coin} on platform {op.platform} for operation 'Withdrawal' anymore.\n"
620-
f"A withdrawal of BEST on bitpanda is likely a deduction of fees. For now we'll assume a value of 0.\n"
621-
f"For accurately calculating fees, this needs to be fixed. PRs welcome!\n"
622-
f"(row {op.line} in {op.file_path}"
639+
f"Could not get any price info for {type(op).__name__} {op.coin} on {op.platform}! "
640+
f"Row: {op.line} | File: {op.file_path}"
623641
)
624-
return 0
625-
raise RuntimeError(f"Can't get price for '{type(op).__name__}' of {op.coin} on platform {op.platform} (row {op.line} in {op.file_path})!")
626-
else:
627-
price = self.get_price(op.platform, op.coin, op.utc_time, reference_coin)
642+
raise RuntimeError(e)
643+
644+
# This may fail if an exchange is queried for a non existant coin/fiat pair and the operation doesn't include an exported price.
645+
assert price, f"Could not get a price for asset {op.coin} at {op.utc_time}"
628646

629647
if isinstance(op_sc, tr.Operation):
630648
return price * op_sc.change

src/taxman.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ def _evaluate_sell(
261261
Raises:
262262
NotImplementedError: When there are more than two different fee coins.
263263
"""
264-
assert op.coin == sc.op.coin
265-
assert op.change >= sc.sold
264+
assert op.coin == sc.op.coin, f"Error evaluating op.coin==sc.op.coin:\n\t\t{op}\n\t\t{sc}"
265+
assert op.change >= sc.sold, f"Error evaluating op.change >=sc.sold:\n\t\t{op}\n\t\t{sc}"
266266

267267
# Share the fees and sell_value proportionally to the coins sold.
268268
percent = sc.sold / op.change
@@ -284,7 +284,7 @@ def _evaluate_sell(
284284
except Exception as e:
285285
if ReportType is tr.UnrealizedSellReportEntry:
286286
log.warning(
287-
"Catched the following exception while trying to query an "
287+
"Caught the following exception while trying to query an "
288288
f"unrealized sell value for {sc.sold} {sc.op.coin} at deadline "
289289
f"on platform {sc.op.platform}. "
290290
"If you want to see your unrealized sell value "
@@ -294,7 +294,7 @@ def _evaluate_sell(
294294
"The sell value for this calculation will be set to 0. "
295295
"Your unrealized sell summary will be wrong and will not "
296296
"be exported.\n"
297-
f"Catched exception: {e}"
297+
f"Caught exception: {type(e).__name__}: {e}"
298298
)
299299
sell_value_in_fiat = decimal.Decimal()
300300
self.unrealized_sells_faulty = True

0 commit comments

Comments
 (0)