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

Detailed export #98

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb83e91
added option to export all events - WIP
Griffsano Dec 26, 2021
f1d68ce
export: added platform, minor format changes
Griffsano Dec 26, 2021
8aad810
address flake8 and mypy errors
Griffsano Dec 26, 2021
6d83bb6
address mypy errors
Griffsano Dec 26, 2021
d2f51f9
address flake8 error
Griffsano Dec 26, 2021
82d2705
renamed sell_price to sell_value to avoid confusion
Griffsano Dec 26, 2021
d8bfeca
remark for German tax calculation
Griffsano Dec 26, 2021
ec6b8ef
added option to include virtual sells in the export
Griffsano Dec 26, 2021
4912360
Merge branch 'main' into detailed-export
Griffsano Jan 16, 2022
f7adc22
moved config to ini
Griffsano Jan 16, 2022
821122f
address review comments
Griffsano Jan 16, 2022
7b6beb2
FIX linting errors
provinzio Feb 5, 2022
55ef433
CHANGE evaluate_sell to return list of TaxEvents
Griffsano Feb 20, 2022
6415130
FIX linting errors
Griffsano Feb 26, 2022
0bef72a
ADD newline in config.ini
provinzio Mar 19, 2022
cb0f5c6
FIX typo
provinzio Mar 19, 2022
5f5a276
REFACTOR code
provinzio Mar 19, 2022
9511d16
FIX Price relation in tax event remark
provinzio Mar 19, 2022
8bb18ed
Merge remote-tracking branch 'origin/main' into detailed-export
provinzio Mar 19, 2022
96b4c07
FIX linting error
provinzio Mar 19, 2022
d347127
FIX is_taxable value for multiple sold coins
provinzio Mar 19, 2022
c383bdc
ADD real_gain of airdrop
provinzio Mar 19, 2022
9d19ecb
ADD configparser country errors
Griffsano Mar 24, 2022
90f2179
ADD is_config_fiat function
Griffsano Mar 24, 2022
189367e
ADD class for config.FIAT
Griffsano Mar 25, 2022
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
4 changes: 4 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@
# Calculate the (taxed) gains, if the left over coins would be sold right now.
# This will fetch the current prices and therefore slow down repetitive runs.
CALCULATE_VIRTUAL_SELL = True
# Include virtual sells in the export
EXPORT_VIRTUAL_SELL = False
# Evaluate taxes for each depot/platform separately. This may reduce your
# taxable gains. Make sure, that this method is accepted by your tax
# authority.
MULTI_DEPOT = True
# Export all events (True) or only taxable events (False)
EXPORT_ALL_EVENTS = True

# Read in environmental variables.
if _env_country := environ.get("COUNTRY"):
Expand Down
144 changes: 107 additions & 37 deletions src/taxman.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def evaluate_sell(op: transaction.Operation) -> Optional[transaction.TaxEvent]:

taxation_type = "Sonstige Einkünfte"
# Price of the sell.
sell_price = self.price_data.get_cost(op)
sell_value = self.price_data.get_cost(op)
taxed_gain = decimal.Decimal()
real_gain = decimal.Decimal()
# Coins which are older than (in this case) one year or
Expand All @@ -126,89 +126,150 @@ def evaluate_sell(op: transaction.Operation) -> Optional[transaction.TaxEvent]:
)
# Only calculate the gains if necessary.
if is_taxable or config.CALCULATE_VIRTUAL_SELL:
partial_sell_price = (sc.sold / op.change) * sell_price
partial_sell_value = (sc.sold / op.change) * sell_value
sold_coin_cost = self.price_data.get_cost(sc)
gain = partial_sell_price - sold_coin_cost
gain = partial_sell_value - sold_coin_cost
if is_taxable:
taxed_gain += gain
if config.CALCULATE_VIRTUAL_SELL:
real_gain += gain
remark = ", ".join(
f"{sc.sold} from {sc.op.utc_time} " f"({sc.op.__class__.__name__})"
f"{sc.sold} von {sc.op.utc_time} " f"({sc.op.__class__.__name__})"
for sc in sold_coins
)
return transaction.TaxEvent(
taxation_type,
taxed_gain,
op,
sell_price,
is_taxable,
sell_value,
real_gain,
remark,
)

for op in operations:
if isinstance(op, transaction.Fee):
balance.remove_fee(op.change)
taxation_type = "Sonstige Einkünfte"
if self.in_tax_year(op):
# Fees reduce taxed gain.
taxation_type = "Sonstige Einkünfte"
taxed_gain = -self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op)
self.tax_events.append(tx)
is_taxable = True
else:
taxation_type += " außerhalb des Steuerjahres"
is_taxable = False
taxed_gain = -self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op, is_taxable)
elif isinstance(op, transaction.CoinLend):
pass
taxation_type = "CoinLend"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.CoinLendEnd):
pass
taxation_type = "CoinLendEnd"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.Staking):
pass
taxation_type = "Staking"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.StakingEnd):
pass
taxation_type = "StakingEnd"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.Buy):
balance.put(op)
if misc.is_fiat(op.coin):
continue
else:
taxation_type = "Kauf"
cost = self.price_data.get_cost(op)
price = self.price_data.get_price(
op.platform,
op.coin,
op.utc_time,
config.FIAT
)
remark = (
f"Kosten {cost} {config.FIAT}, "
f"Preis {price} {config.FIAT}/{op.coin}"
)
tx = transaction.TaxEvent(
taxation_type,
decimal.Decimal(),
op,
False,
decimal.Decimal(),
decimal.Decimal(),
remark
)
elif isinstance(op, transaction.Sell):
if misc.is_fiat(op.coin):
continue
if tx_ := evaluate_sell(op):
self.tax_events.append(tx_)
tx = tx_
else:
taxation_type = (
"Verkauf "
"(außerhalb des Steuerjahres oder steuerfrei)"
)
tx = transaction.TaxEvent(
taxation_type,
decimal.Decimal(),
op,
False
)
Copy link
Owner

@provinzio provinzio Mar 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluate_sell shouldn't return None if we want to export all events. evaluate_sell should calculate the real_gain, which currently missing.

elif isinstance(
op, (transaction.CoinLendInterest, transaction.StakingInterest)
):
balance.put(op)
if misc.is_fiat(coin):
assert not isinstance(
op, transaction.StakingInterest
), "You can not stake fiat currencies."
taxation_type = "Einkünfte aus Kapitalvermögen"
else:
taxation_type = "Einkünfte aus sonstigen Leistungen"
if self.in_tax_year(op):
if misc.is_fiat(coin):
assert not isinstance(
op, transaction.StakingInterest
), "You can not stake fiat currencies."
taxation_type = "Einkünfte aus Kapitalvermögen"
else:
taxation_type = "Einkünfte aus sonstigen Leistungen"
taxed_gain = self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op)
self.tax_events.append(tx)
is_taxable = True
else:
taxation_type += " außerhalb des Steuerjahres"
is_taxable = False
taxed_gain = self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op, is_taxable)
elif isinstance(op, transaction.Airdrop):
balance.put(op)
taxation_type = "Airdrop"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.Commission):
balance.put(op)
taxation_type = "Einkünfte aus sonstigen Leistungen"
if self.in_tax_year(op):
taxation_type = "Einkünfte aus sonstigen Leistungen"
taxed_gain = self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op)
self.tax_events.append(tx)
is_taxable = True
else:
taxation_type += " außerhalb des Steuerjahres"
is_taxable = False
taxed_gain = self.price_data.get_cost(op)
tx = transaction.TaxEvent(taxation_type, taxed_gain, op, is_taxable)
elif isinstance(op, transaction.Deposit):
if coin != config.FIAT:
log.warning(
f"Unresolved deposit of {op.change} {coin} "
f"on {op.platform} at {op.utc_time}. "
"The evaluation might be wrong."
)
taxation_type = "Deposit"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
elif isinstance(op, transaction.Withdrawal):
if coin != config.FIAT:
log.warning(
f"Unresolved withdrawal of {op.change} {coin} "
f"from {op.platform} at {op.utc_time}. "
"The evaluation might be wrong."
)
taxation_type = "Withdrawal"
tx = transaction.TaxEvent(taxation_type, decimal.Decimal(), op, False)
else:
raise NotImplementedError

# for all valid cases, add tax event to list
if tx:
self.tax_events.append(tx)

# Check that all relevant positions were considered.
if balance.buffer_fee:
log.warning(
Expand Down Expand Up @@ -282,12 +343,12 @@ def print_evaluation(self) -> None:
# Summarize the virtual sell, if all left over coins would be sold right now.
if self.virtual_tax_events:
assert config.CALCULATE_VIRTUAL_SELL
invsted = sum(tx.sell_price for tx in self.virtual_tax_events)
invested = sum(tx.sell_value for tx in self.virtual_tax_events)
real_gains = sum(tx.real_gain for tx in self.virtual_tax_events)
taxed_gains = sum(tx.taxed_gain for tx in self.virtual_tax_events)
print()
print(
f"You are currently invested with {invsted:.2f} {config.FIAT}.\n"
f"You are currently invested with {invested:.2f} {config.FIAT}.\n"
f"If you would sell everything right now, "
f"you would realize {real_gains:.2f} {config.FIAT} gains "
f"({taxed_gains:.2f} {config.FIAT} taxed gain)."
Expand All @@ -296,13 +357,13 @@ def print_evaluation(self) -> None:
print("Your current portfolio should be:")
for tx in sorted(
self.virtual_tax_events,
key=lambda tx: tx.sell_price,
key=lambda tx: tx.sell_value,
reverse=True,
):
print(
f"{tx.op.platform}: "
f"{tx.op.change:.6f} {tx.op.coin} > "
f"{tx.sell_price:.2f} {config.FIAT} "
f"{tx.sell_value:.2f} {config.FIAT} "
f"({tx.real_gain:.2f} gain, {tx.taxed_gain:.2f} taxed gain)"
)

Expand Down Expand Up @@ -333,29 +394,38 @@ def export_evaluation_as_csv(self) -> Path:
writer.writerow(["# updated", datetime.date.today().strftime("%x")])

header = [
"Date",
"Date and Time UTC",
"Platform",
"Taxation Type",
f"Taxed Gain in {config.FIAT}",
"Action",
"Amount",
"Asset",
f"Sell Price in {config.FIAT}",
f"Sell Value in {config.FIAT}",
"Remark",
]
writer.writerow(header)

if config.EXPORT_VIRTUAL_SELL:
# move virtual sells to tax_events list
self.tax_events = self.tax_events + self.virtual_tax_events
self.virtual_tax_events = []

# Tax events are currently sorted by coin. Sort by time instead.
for tx in sorted(self.tax_events, key=lambda tx: tx.op.utc_time):
line = [
tx.op.utc_time,
tx.op.utc_time.strftime("%Y-%m-%d %H:%M:%S"),
tx.op.platform,
tx.taxation_type,
tx.taxed_gain,
tx.op.__class__.__name__,
tx.op.change,
tx.op.coin,
tx.sell_price,
tx.sell_value,
tx.remark,
]
writer.writerow(line)
if tx.is_taxable or config.EXPORT_ALL_EVENTS:
writer.writerow(line)

log.info("Saved evaluation in %s.", file_path)
return file_path
3 changes: 2 additions & 1 deletion src/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ class TaxEvent:
taxation_type: str
taxed_gain: decimal.Decimal
op: Operation
sell_price: decimal.Decimal = decimal.Decimal()
is_taxable: bool = True
sell_value: decimal.Decimal = decimal.Decimal()
real_gain: decimal.Decimal = decimal.Decimal()
remark: str = ""

Expand Down