1
0
crypto-tax/crypto_tax/__init__.py

200 lines
6.6 KiB
Python
Raw Normal View History

2024-12-14 20:12:18 +00:00
"""# Crypto Tax.
2024-12-22 05:25:07 +00:00
Tool to automatically perform tax computations of crypto transactions from the danish perspective.
## TODO
This tool is a work in progress:
- [ ] Support Partisia Blockchain
- [ ] Fix Kucoin issues.
- [ ] Produce Excel output file:
* Sheet 1: Gives a basic overview, including computed tax values from the rest of
the sheet, and reasonings for these. Gives an overview of the other sheets.
* Sheet 2: Current assets, including FIFO acquisition prices.
* Sheet 3: Historic sales, with FIFO.
2024-12-14 20:12:18 +00:00
"""
2024-12-22 05:10:23 +00:00
import sys
import requests
import fin_depo
from . import secrets
from fin_defs import CryptoCurrency, AssetAmount, MPC, Asset, USDT
from decimal import Decimal
from collections import deque
from fin_depo.data import *
import datetime
2024-12-28 00:02:37 +00:00
import dataclassabc
2024-12-22 05:10:23 +00:00
import dataclasses
import logging
2024-12-22 22:25:24 +00:00
import fin_data
2024-12-22 05:10:23 +00:00
2024-12-24 19:12:50 +00:00
from .data import TaxReport, BoughtAndSold, BoughtAndNotYetSold, LedgerEntry
2024-12-22 05:10:23 +00:00
2024-12-27 21:14:39 +00:00
try:
import logging_color
logging_color.monkey_patch()
except ImportError as e:
logging.warning('Color logging not enabled')
del e
2024-12-22 22:25:24 +00:00
logger = logging.getLogger(__name__)
2024-12-22 05:10:23 +00:00
2024-12-22 22:25:24 +00:00
FIN_DB = fin_data.FinancialDatabase(enable_kucoin=True, enable_nationalbanken_dk=True)
2024-12-22 05:10:23 +00:00
2024-12-28 00:02:37 +00:00
@dataclassabc.dataclassabc(frozen=True)
class TransferDetails(fin_depo.data.DoubleRegister):
withdrawal: fin_depo.data.WithdrawalDetails
deposit: fin_depo.data.DepositDetails
@property
def fee(self) -> AssetAmount | None:
return self.withdrawal.fee # TODO?
@property
def executed_time(self) -> datetime.datetime:
return self.withdrawal.executed_time
@property
def input(self) -> None:
return None
@property
def output(self) -> None:
return None
def collate_interaccount_transfers(transfers: list[fin_depo.data.DoubleRegister]) -> list[fin_depo.data.DoubleRegister]:
new_transfers = []
for t in transfers:
if isinstance(t, fin_depo.data.DepositDetails):
prev_transfer = new_transfers[-1] if len(new_transfers) > 0 else None
if isinstance(prev_transfer , fin_depo.data.WithdrawalDetails):
if prev_transfer.withdrawn == t.deposit:
del new_transfers[-1]
new_transfers.append(TransferDetails(prev_transfer, t))
continue
del prev_transfer
new_transfers.append(t)
del t
return new_transfers
2024-12-22 05:10:23 +00:00
2024-12-24 19:12:50 +00:00
def compute_fifo(transfers: list) -> TaxReport:
2024-12-22 05:10:23 +00:00
transfers.sort(key=lambda t:t.executed_time)
2024-12-28 00:02:37 +00:00
transfers = collate_interaccount_transfers(transfers)
2024-12-22 05:10:23 +00:00
bought_and_sold_for: list[BoughtAndSold] = []
bought_and_spent_for: list[BoughtAndSold] = []
2024-12-24 19:12:50 +00:00
ledger_entries = []
2024-12-22 05:10:23 +00:00
prices_bought_for: dict[Asset, deque[BoughtAndNotYetSold]] = {}
2024-12-28 00:02:37 +00:00
if True:
2024-12-22 05:10:23 +00:00
prices_bought_for[MPC] = deque()
2024-12-24 19:55:16 +00:00
# TODO:
2024-12-27 14:58:38 +00:00
prices_bought_for[MPC].append(BoughtAndNotYetSold(AssetAmount(MPC, Decimal(80)), datetime.datetime(2024, 4,1,1,1,1,1)))
2024-12-22 05:10:23 +00:00
prices_bought_for[USDT] = deque()
2024-12-24 19:55:16 +00:00
prices_bought_for[USDT].append(BoughtAndNotYetSold(AssetAmount(USDT, Decimal(20)), datetime.datetime(2020, 1,1,1,1,1,1)))
2024-12-22 05:10:23 +00:00
2024-12-24 19:12:50 +00:00
def current_balance(asset: Asset) -> AssetAmount:
return sum((p.amount for p in prices_bought_for[asset]), start=AssetAmount.ZERO)
def sell(amount_to_sell: AssetAmount, executed_time: datetime.datetime, variant: str = 'SELL'):
2024-12-27 21:14:39 +00:00
logger.debug('%s: %s', variant, amount_to_sell)
2024-12-24 19:12:50 +00:00
amount = amount_to_sell
2024-12-22 05:10:23 +00:00
while amount.amount > 0:
if len(prices_bought_for[amount.asset]) == 0:
2024-12-28 00:02:37 +00:00
msg = f'Miscalculation: {amount} of {amount_to_sell}'
raise Exception(msg)
2024-12-22 05:10:23 +00:00
partial = prices_bought_for[amount.asset].popleft()
amount_covered_by_partial = min(partial.amount, amount)
amount -= amount_covered_by_partial
new_partial_amount = partial.amount - amount_covered_by_partial
# Re-add partial
if new_partial_amount.amount != 0:
new_partial = BoughtAndNotYetSold(new_partial_amount, partial.time_bought)
prices_bought_for[amount.asset].appendleft(new_partial)
del new_partial
# Add FIFO like
2024-12-24 19:12:50 +00:00
if variant == 'FEE':
2024-12-22 05:10:23 +00:00
bought_and_spent_for.append(BoughtAndSold(amount_covered_by_partial, partial.time_bought, executed_time))
else:
bought_and_sold_for.append(BoughtAndSold(amount_covered_by_partial, partial.time_bought, executed_time))
del partial, amount_covered_by_partial
2024-12-24 19:12:50 +00:00
ledger_entries.append(LedgerEntry(
time = executed_time,
type = variant,
account_provider = 'Kraken (TODO)',
2024-12-24 19:55:16 +00:00
amount = -amount_to_sell,
2024-12-24 19:12:50 +00:00
balance = current_balance(amount.asset),
))
2024-12-22 05:10:23 +00:00
def spend(amount: AssetAmount, executed_time: datetime.datetime):
2024-12-24 19:12:50 +00:00
if amount.amount > 0:
sell(amount, executed_time, 'FEE')
def buy(bought: AssetAmount, executed_time: datetime.datetime, variant: str):
logger.debug('Bought: %s', bought)
if bought.asset not in prices_bought_for:
prices_bought_for[bought.asset] = deque()
prices_bought_for[bought.asset].append(BoughtAndNotYetSold(bought, executed_time))
ledger_entries.append(LedgerEntry(
time = executed_time,
type = variant,
account_provider = 'Kraken (TODO)',
amount = bought,
balance = current_balance(bought.asset),
))
2024-12-22 05:10:23 +00:00
for transfer in transfers:
2024-12-28 00:02:37 +00:00
2024-12-22 05:10:23 +00:00
# Sell
if _input := transfer.input:
2024-12-24 19:12:50 +00:00
variant = 'WITHDRAW' if transfer.output is None else 'SELL'
sell(_input, transfer.executed_time, variant)
del variant
2024-12-22 05:10:23 +00:00
del _input
2024-12-24 19:12:50 +00:00
# Buy
if output := transfer.output:
variant = 'DEPOSIT' if transfer.input is None else 'BUY'
buy(output, transfer.executed_time, variant)
del variant
del output
2024-12-28 00:02:37 +00:00
# TODO: Output line for transfer.
2024-12-24 19:12:50 +00:00
# Fee
2024-12-22 05:10:23 +00:00
if fee := transfer.fee:
spend(fee, transfer.executed_time)
2024-12-24 19:12:50 +00:00
del fee
del transfer
2024-12-22 05:10:23 +00:00
2024-12-22 22:25:24 +00:00
def exchange_rate_at_time(a, b, t):
try:
sample = FIN_DB.get_exchange_rate_at_time(a, b, t)
return sample.average
except Exception:
logger.exception('Error when fetching exchange rate (%s,%s) at %s', a, b, t)
return None
2024-12-22 05:10:23 +00:00
return TaxReport(
bought_and_sold_for=bought_and_sold_for,
bought_and_spent_for=bought_and_spent_for,
current_assets=prices_bought_for,
2024-12-22 22:25:24 +00:00
exchange_rate_at_time=exchange_rate_at_time,
2024-12-24 19:12:50 +00:00
ledger_entries = ledger_entries,
2024-12-22 05:10:23 +00:00
)