Customization of ccGains for the generation of a yearly Crypto Report in jurisdictions where there is tax exemption for crypto holdings held for one year or more

Für Deutsch hier

Read on Medium

Image by fabrikasimf on Freepik
Introduction

In the dynamic world of cryptocurrency, where volatility and innovation intersect, the regulatory landscape is continuously evolving. One notable aspect of this evolution is the taxation of cryptocurrency gains. While many countries have implemented various tax regimes for cryptocurrencies, some offer favorable policies that exempt long-term holders from certain tax liabilities. One such policy gaining traction in several jurisdictions is the tax exemption for crypto holdings held for one year or more.

What is the One-Year Holding Policy?

The one-year holding policy, also known as long-term capital gains treatment, is a taxation framework that grants exemptions or reduced tax rates on profits generated from the sale or exchange of cryptocurrencies held for a minimum duration of one year. This policy aims to incentivize long-term investment in cryptocurrencies while encouraging stability in the market.

Countries with Tax-Free Crypto Gains Policy

Several countries have embraced the concept of tax-free crypto gains for long-term holders. Some of these nations include:

  1. Germany: Germany has emerged as one of the frontrunners in providing clarity on cryptocurrency taxation. In Germany, if you hold your cryptocurrencies for more than one year, any resulting gains are completely tax-free.
  2. Portugal: Portugal has gained attention for its tax-friendly policies towards cryptocurrencies. The country does not tax individuals on capital gains from the sale of cryptocurrencies if they have been held for more than one year.
  3. Singapore: Singapore is renowned for its pro-business environment and has adopted a similar approach to cryptocurrencies. Capital gains from long-term cryptocurrency holdings are not taxed in Singapore, provided certain conditions are met.
  4. Belarus: Belarus has implemented legislation that exempts individuals and businesses from taxes on cryptocurrency transactions, including capital gains, until 2023. This policy aims to attract investment and innovation in the country’s emerging tech sector.
  5. Switzerland: Switzerland, known for its favorable regulatory environment for finance and technology, treats cryptocurrencies as assets rather than currencies for tax purposes. Consequently, capital gains from long-term cryptocurrency holdings are typically tax-free for individuals.
What is ccGains?

The ccGains (cryptocurrency gains) package provides a python library for calculating capital gains made by trading cryptocurrencies or foreign currencies. It was created by Jürgen Probst and it is hosted in Github (link here). Some of its features are:

  • Calculates the capital gains using the first-in/first out (FIFO) principle, • creates capital gains reports as CSV, HTML or PDF (instantly ready to print out for the tax office),
  • Can create a more detailed capital gains report outlining the calculation and used bags,
  • Differs between short and long term gains (amounts held for less or more than a year),
  • Treats amounts held and traded on different exchanges separately,
  • Treats exchange fees and transaction fees directly resulting from trading properly as losses,
  • Provides methods to import your trading history from various exchanges,
  • Loads historic cryptocurrency prices from CSV files and/or
  • Loads historic prices from APIs provided by exchanges,
  • Caches historic price data on disk for quicker and offline retrieval and less traffic to exchanges,
  • For highest accuracy, uses the decimal data type for all amounts
  • Supports saving and loading the state of your portfolio as JSON file for use in ccGains calculations in following years
Installation

You’ll need Python (ccGains is tested under Python 2.7 and Python 3.x). Get it here: https://www.python.org/

ccGains can then easily be installed via the Python package manager pip:

  • Download the source code, e.g. by git clone https://github.com/probstj/ccgains.git
  • Inside the main ccGains directory run: pip install . (note the . at the end)
  • Alternatively, to install locally without admin rights: pip install --user .
  • And if you want to add changes to the source code and quickly want to try it without reinstalling, pip can install by linking to the source code folder: pip install -e .
Usage

Please have a look at examples/example.py and follow the comments to adapt it for your purposes.

Copy it to a new file for example taxReport2023.py.

1. Provide list of BTC historical prices in your native fiat currency

Hourly data for a lot of exchanges is available for download at:
https://api.bitcoincharts.com/v1/csv/
To understand which file to download, consult this list:
https://bitcoincharts.com/markets/list/

For EUR prices on Kraken, download: https://api.bitcoincharts.com/v1/csv/krakenEUR.csv.gz and place it in the ../data folder.

For CHF prices, download: https://api.bitcoincharts.com/v1/csv/anxhkCHF.csv.gz

For SGD prices, download: https://api.bitcoincharts.com/v1/csv/anxhkSGD.csv.gz

The file consists of three comma-separated columns: the unix timestamp, the price, and the volume (amount traded).

Create the HistoricData object by loading the mentioned file and
specifying the price unit, i.e. fiat/btc:

h1 = ccgains.HistoricDataCSV(
'../data/bitcoin_de_EUR_abridged_as_example.csv.gz', 'EUR/BTC')

2. Provide source of historical BTC prices for all traded alt coins

For all coins that you possessed at some point, their historical price in your native fiat currency must be known, which can also be derived from their BTC price and the BTC/fiat price given above (or even from their price in any other alt-coin, whose price can be derived, in turn.)

This data can be provided from any website that serves this data through an API, or from a csv-file, like above. Note that currently, only the API from Poloniex.com is implemented.

Create HistoricData objects to fetch rates from Poloniex.com: (it is important to mention at least all traded coins here)

    h2 = ccgains.HistoricDataAPI('data', 'btc/xmr')
h3 = ccgains.HistoricDataAPI('data', 'btc/eth')
h4 = ccgains.HistoricDataAPI('data', 'btc/usdt')
h5 = ccgains.HistoricDataAPI('data', 'btc/link')
h6 = ccgains.HistoricDataAPI('data', 'btc/bat')
h7 = ccgains.HistoricDataAPI('data', 'btc/zrx')
h8 = ccgains.HistoricDataAPI('data', 'btc/cvc')
h9 = ccgains.HistoricDataAPI('data', 'btc/dash')
h10 = ccgains.HistoricDataAPI('data', 'btc/knc')
h11 = ccgains.HistoricDataAPI('data', 'btc/mkr')
h12 = ccgains.HistoricDataAPI('data', 'btc/matic')
h13 = ccgains.HistoricDataAPI('data', 'btc/doge')
h14 = ccgains.HistoricDataAPI('data', 'btc/bch')
h15 = ccgains.HistoricDataAPI('data', 'btc/dot')
h16 = ccgains.HistoricDataAPI('data', 'btc/qtum')
h17 = ccgains.HistoricDataAPI('data', 'btc/ren')
h18 = ccgains.HistoricDataAPI('data', 'btc/str')
h19 = ccgains.HistoricDataAPI('data', 'btc/xtz')
h20 = ccgains.HistoricDataAPI('data', 'btc/trx')
h21 = ccgains.HistoricDataAPI('data', 'btc/zec')
h22 = ccgains.HistoricDataAPI('data', 'btc/ltc')
h23 = ccgains.HistoricDataAPI('data', 'btc/xrp')
h24 = ccgains.HistoricDataAPI('data', 'btc/omg')
h25 = ccgains.HistoricDataAPI('data', 'btc/etc')
h26 = ccgains.HistoricDataAPI('data', 'btc/dot')
h27 = ccgains.HistoricDataAPI('data', 'btc/dai')
h28 = ccgains.HistoricDataAPI('data', 'btc/usdc')
h29 = ccgains.HistoricDataAPICoinbase('data', 'cro/eur')
h30 = ccgains.HistoricDataAPIBinance('data', 'btc/uni')
h31 = ccgains.HistoricDataAPIBinance('data', 'avax/eur')
h32 = ccgains.HistoricDataAPIBinance('data', 'btc/dydx')
h33 = ccgains.HistoricDataAPIBinance('data', 'btc/iota')
h34 = ccgains.HistoricDataAPIBinance('data', 'btc/axs')

In h2 to h28 I used the class HistoricDataAPI which uses the public Poloniex API: https://poloniex.com/public?command=returnTradeHistory, since this is the exchange that has these pairs traded in the year 2023 in this example.

In h29 I used the class HistoricDataAPICoinbase which uses the public Coinbase API: ‘https://api.pro.coinbase.com/products/:SYMBOL:/candles’.

In h30 to h34 I used the class HistoricDataAPIBinance which will transparently fetch data on request (get_price) from the public Binance API: https://api.binance.com/api/v1/klines

For faster loading times on future calls, a HDF5 file is created from the requested data and used transparently the next time a request for the same day and pair is made. These HDF5 files are saved in cache_folder. The unit must be a string in the form ‘currency_one/currency_two’, e.g. ‘NEO/BTC’. The data will be resampled by calculating the weighted price for interval steps specified by interval. See: http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases for possible values.

prepare_request(dtime) Return a pandas DataFrame which contains the data for the requested datetime dtime.

3. Add all objects from above into a single ‘CurrencyRelation’ object

Create a CurrencyRelation object that puts all provided HistoricData currencies in relation in order to serve exchange rates for any pair of these currencies:

rel = ccgains.CurrencyRelation(h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, h11, h12, h13, h14, h15, h16, h17, h18, h19, h20, h21, h22, h23, h24, h25, h26,h27, h28, h29, h30, h31, h32, h33, h34)

4. Create the ‘BagQueue’, which calculates the capital gains

Create the BagQueue object that calculates taxable profit from trades using the first-in/first-out method:

(this needs to know your native fiat currency and the CurrencyRelation created above)

bf = ccgains.BagQueue('EUR', rel)

5. Create the object that will load all your trades

The TradeHistory object provides methods to load your trades from csv-files exported from various exchanges or apps.

 th = ccgains.TradeHistory()

6. Load all your trades from csv-files

Export your trades from exchanges or apps as comma-separated files and append them to the list of trades managed by the TradeHistory object. All trades will be sorted automatically.

To load from a supported exchange, use the methods named `append_<exchange_name>_csv` found in TradeHistory (see trades.py).

    th.append_poloniex_csv(
'./data/2020/poloniex_depositHistory_2023.csv',
'deposits')
th.append_poloniex_csv(
'./data/2020/poloniex_tradeHistory_2023.csv',
'trades',
condense_trades=True)
th.append_poloniex_csv(
'./data/2020/poloniex_withdrawalHistory_2023.csv',
'withdr')

th.append_binance_csv(
'./data/2020/binance_depositHistory_2023.csv',
'deposits')
th.append_binance_csv(
'./data/2020/binance_tradeHistory_2023.csv',
'deposits')
th.append_binance_csv(
'./data/2020/binance_withdrawalHistory_2023.csv',
'deposits')
th.append_wirex_csv(
'./data/2020/wirex_btc_tradeHistory_2023.csv',
'trades')
th.append_cro_csv(
'./data/2021/cro_tradeHistory_2023.csv',
'trades')

If your exchange is not supported yet, add a new method in trades.py. In this example the methods append_wirex_csv and append_cro_csv are not supported in the original GitHub project.

Next I will show how I did it with append_cro_csv (Crypto.com). First import TPLOC_CRO_TRADES, which is the library that knows how to read the CSV file from the Crypto.com exchange.

from .cro_util import (
TPLOC_CRO_TRADES)
 def append_cro_csv(
self, file_name, which_data='trades', delimiter=',',
skiprows=1, default_timezone=tz.tzutc()):

wdata = which_data[:5].lower()
if wdata not in ['trade']:
raise ValueError(
'`which_data` must be one of "trades"')

plocs = TPLOC_CRO_TRADES
self.append_csv(
file_name=file_name,
param_locs=plocs,
delimiter=delimiter,
skiprows=skiprows,
default_timezone=default_timezone
)

Now create the cro_util.py file

from decimal import Decimal

def kind_for(csv_line):
if 'Withdraw' in (csv_line[1].strip('" \n\t')) or ('Transfer' in (csv_line[1].strip('" \n\t')) and 'App' in csv_line[1].strip('" \n\t').split("->")[0]):
return 'Withdrawal'
elif 'Deposit' in (csv_line[1].strip('" \n\t')) or 'Reward' in (csv_line[1].strip('" \n\t')) or ('Transfer' in (csv_line[1].strip('" \n\t')) and 'Exchange' in csv_line[1].strip('" \n\t').split("->")[0]):
return 'Deposit'
elif 'Buy' in (csv_line[1].strip('" \n\t')):
return 'Buy'
elif '->' in csv_line[1].strip('" \n\t'):
return 'Sell'
else:
return None

def get_buy_currency(csv_line):
if not '->' in csv_line[1].strip('" \n\t') and (kind_for(csv_line) == 'Buy' or kind_for(csv_line) == 'Deposit' or 'App' in csv_line[1].strip('" \n\t').split("->")[0]):
return csv_line[2].strip('" \n\t')
elif '->' in csv_line[1].strip('" \n\t') and (kind_for(csv_line) == 'Buy' or kind_for(csv_line) == 'Deposit' or 'App' in csv_line[1].strip('" \n\t').split("->")[0]):
return csv_line[2].strip('" \n\t')
else:
return csv_line[1].strip('" \n\t').split("->")[1]

def get_sell_currency(csv_line):
if not '->' in csv_line[1].strip('" \n\t') and (kind_for(csv_line) == 'Sell' or kind_for(csv_line) == 'Withdrawal' or 'Exchange' in csv_line[1].strip('" \n\t').split("->")[0]):
return csv_line[2].strip('" \n\t')
elif '->' in csv_line[1].strip('" \n\t') and (kind_for(csv_line) == 'Sell' or kind_for(csv_line) == 'Withdrawal' or 'Exchange' in csv_line[1].strip('" \n\t').split("->")[0]):
return csv_line[2].strip('" \n\t')
else:
return csv_line[1].strip('" \n\t').split("->")[0]

#Trade parameters in csv from Crypto.com
TPLOC_CRO_TRADES = {
'kind': lambda cols: kind_for(cols),
'dtime': 0,
'buy_currency': lambda cols: get_buy_currency(cols) if kind_for(cols) == 'Buy' or kind_for(cols) == 'Deposit' else cols[4] if cols[4].strip('" \n\t') != '' else '',
'buy_amount': lambda cols: abs(Decimal(cols[3])) if kind_for(cols) == 'Buy' or kind_for(cols) == 'Deposit' else abs(Decimal(cols[5])) if cols[5].strip('" \n\t') != '' else '',
'sell_currency': lambda cols: get_sell_currency(cols) if kind_for(cols) == 'Sell' or kind_for(cols) == 'Withdrawal' else 'EUR',
'sell_amount': lambda cols: abs(Decimal(cols[3])) if kind_for(cols) == 'Sell' or kind_for(cols) == 'Withdrawal' else cols[7],
'fee_currency': -1,
'fee_amount': -1,
'exchange': 'Crypto.com', 'mark': -1,
'comment': lambda cols: cols[1]
}

7. Optionally, fix withdrawal fees

Some exchanges, like Poloniex, does not include withdrawal fees in their exported csv files. This will try to add these missing fees by comparing withdrawn amounts with amounts deposited on other exchanges shortly after withdrawal. Call this only after all transactions from every involved exchange and wallet were imported.

This uses a really simple algorithm, so it is not guaranteed to work in every case, especially if you made withdrawals in tight succession on different exchanges, so please check the output.

th.add_missing_transaction_fees(raise_on_error=False)

8. Optionally, rename currencies

Some currencies have changed ticker symbols since their first listing date (e.g., AntShares (ANS) -> Neo (NEO)). This can lead to situations where all historical pricing data lists the new ticker symbol, but transaction history still lists the old ticker.

This method allows for renaming symbols in the TradeHistory, if any occurrences of the old name/ticker are found.

th.update_ticker_names({'ANS': 'NEO'})

9. Optionally, export all trades for future reference

You can export all imported trades for future reference into a single file, optionally filtered by year.

…either as a comma-separated text file (can be imported into ccgains):

th.export_to_csv('transactions2023.csv', year=2023)

…or as html or pdf file, with the possibility to filter or rename column headers or contents:
(This is an example for a translation into German)

 my_column_names=[
'Art', 'Datum', 'Kaufmenge', 'Verkaufsmenge', u'Gebühren', u'Börse']
transdct = {'Buy': 'Anschaffung',
'BUY': 'Anschaffung',
'Sell': 'Tausch',
'SELL': 'Tausch',
'Purchase': 'Anschaffung',
'Exchange': 'Tausch', 'Disbursement': 'Abhebung',
'Deposit': 'Einzahlung',
'Withdrawal': 'Abhebung',
'Received funds': 'Einzahlung',
'Withdrawn from wallet': 'Abhebung',
'Create offer fee: a5ed7482': u'Börsengebühr',
'Buy BTC' : 'Anschaffung',
'MultiSig deposit: a5ed7482': 'Abhebung',
'MultiSig payout: a5ed7482' : 'Einzahlung'}
th.export_to_pdf('Transactions2021.pdf',
year=2021, drop_columns=['mark', 'comment'],
font_size=12,
caption=u"Handel mit digitalen Währungen %(year)s",
intro=u"<h4>Auflistung aller Transaktionen zwischen "
"%(fromdate)s und %(todate)s:</h4>",
locale="de_DE",
custom_column_names=my_column_names,
custom_formatters={
'Art': lambda x: transdct[x] if x in transdct else x})

10. Now, finally, the calculation is ready to start

If the calculation run for previous years already, we can load the state of the bags here, no need to calculate everything again:

bf.load('./status2022.json')

Or, if the current calculation crashed (e.g. you forgot to add a traded currency in #2 above), the file ‘precrash.json’ will be created automatically. Load it here to continue:

bf.load('./precrash.json')

The following just looks where to start calculating trades, in case you already calculated some and restarted by loading ‘precrash.json’:

last_trade = 0
while (last_trade < len(th.tlist)
and th[last_trade].dtime <= bf._last_date):
last_trade += 1
if last_trade > 0:
logger.info("continuing with trade #%i" % (last_trade + 1))

# Now, the calculation. This goes through your imported list of trades:
for i, trade in enumerate(th.tlist[last_trade:]):
# Most of this is just the log output to the console and to the
# file 'ccgains_<date-time>.log'
# (check out this file for all gory calculation details!):
logger.info('TRADE #%i', i + last_trade + 1)
logger.info(trade)
# This is the important part:
bf.process_trade(trade)
# more logging:
log_bags(bf)
logger.info("Totals: %s", str(bf.totals))
logger.info("Gains (in %s): %s\n" % (bf.currency, str(bf.profit)))

11. Save the state of your holdings for the calculation due next year

bf.save('status2023.json')

12. Create your capital gains report for cryptocurrency trades

The default column names used in the report don’t look very nice: [‘kind’, ‘bag_spent’, ‘currency’, ‘bag_date’, ‘sell_date’, ‘exchange’, ‘short_term’, ‘spent_cost’, ‘proceeds’, ‘profit’], so we rename them:

my_column_names=[
'Type', 'Amount spent', u'Currency', 'Purchase date',
'Sell date', u'Exchange', u'Short term', 'Purchase cost',
'Proceeds', 'Profit']

Here we create the report pdf for capital gains in 2023.

The date_precision=’D’ means we only mention the day of the trade, not the precise time. We also set combine=True, so multiple trades made on the same day and on the same exchange are combined into a single trade on the report:

 my_column_names=[
'Art', 'Verkaufsmenge', u'Währung', 'Erwerbsdatum',
'Verkaufsdatum', u'Börse', u'in\xa0Besitz',
'Anschaffungskosten', u'Verkaufserlös', 'Gewinn']
transdct = {'sale': u'Veräußerung',
'withdrawal fee': u'Börsengebühr',
'deposit fee': u'Börsengebühr',
'exchange fee': u'Börsengebühr'}
convert_short_term=[u'>\xa01\xa0Jahr', u'<\xa01\xa0Jahr']

bf.report.export_report_to_pdf(
'Report2021_de.pdf', year=2023,
date_precision='D', combine=True,
custom_column_names=my_column_names,
custom_formatters={
u'in\xa0Besitz': lambda b: convert_short_term[b],
'Art': lambda x: transdct[x]},
locale="de_DE",
template_file='shortreport_de.html'
)
# If you rather want your report in a spreadsheet, you can export
# to csv:
bf.report.export_short_report_to_csv(
'report_2023.csv', year=2023,
date_precision='D', combine=False,
convert_timezone=True, strip_timezone=True)

13. Optional: Create a detailed report outlining the calculation

The simple capital gains report created above is just a plain listing of all trades and the gains made, enough for the tax report.

A more detailed listing outlining the calculation is also available:

bf.report.export_extended_report_to_pdf(
'Details_2023.pdf', year=2023,
date_precision='S', combine=False,
font_size=10, locale="en_US")

And again, let’s translate this report to German: (Using transdct from above again to translate the payment kind)

    bf.report.export_extended_report_to_pdf(
'Details_2023_de.pdf', year=2023,
date_precision='S', combine=False,
font_size=10, locale="de_DE",
template_file='fullreport_de.html',
payment_kind_translation=transdct)

Now run

python taxReport2023.py

This should generate the pdf files Details_2023.pdf and Details_2023_de.pdf, which are the reports needed by the tax authorities.

For more information on crypto tax report generation or customization to your needs, contact us at lnsolutions.ee

References

GitHub - probstj/ccGains: Python package for calculating cryptocurrency trading profits and…

Python package for calculating cryptocurrency trading profits and creating capital gains reports - probstj/ccGains

github.com

https://readthedocs.org/projects/ccgains/downloads/pdf/latest/