Skip to content

Commit

Permalink
Accounting tool: new transaction types Lost and Airdrop added
Browse files Browse the repository at this point in the history
Added new transaction type 'Airdrop', this is purely descriptive, it behaves the same as a 'Gift-Received' so assumes that no income tax is payable for this specific airdrop. For airdrops which require income tax, please use 'Income' instead.

Another new transaction type 'Lost' has also been added. This is to be used if tokens have been lost or stolen (i.e. private keys are unrecoverable) and a "negligible value claim" has been reported/accepted by HMRC.

The lost tokens are treated as a disposal (for the quantity and value as agreed with HMRC) followed by a re-acquisition for the quantity lost, since technically you still own the tokens. If a value is not specified, a default disposal value of zero is assumed.

A new config item 'lost_buyback' has been added which is a boolean, this controls whether a 'Lost' transaction results in a re-acquision (default True) so could be used in the future for tax rules for other countries.

The Accointing and CoinTracking parsers have been updated to map to these new transaction types.

The Binance, Circle and Poloniex parsers have been updated to use Airdrop instead of Gift-Received where applicable.

The RECAP CSV format has been updated to map the new transaction types to the Recap.io equivalents.
  • Loading branch information
nanonano committed Jul 12, 2021
1 parent 92b8515 commit d33d5d9
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ data_source_fiat:
- Conversion tool: added parser for Helium wallet and explorer.
- Conversion tool: added parser for Accointing accounting data.
- Bitfinex parser: new "movements" data file format added.
- Accounting tool: new transaction types Lost and Airdrop added.
### Changed
- Conversion tool: UnknownAddressError exception changed to generic DataFilenameError.
- Binance parser: use filename to determine if deposits or withdrawals.
Expand Down
1 change: 1 addition & 0 deletions bittytax/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Config(object):
'transfers_include': True,
'transfer_fee_disposal': False,
'transfer_fee_allowable_cost': False,
'lost_buyback': True,
'data_source_select': {},
'data_source_fiat': DATA_SOURCE_FIAT,
'data_source_crypto': DATA_SOURCE_CRYPTO,
Expand Down
2 changes: 2 additions & 0 deletions bittytax/conv/out_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ class TransactionOutRecord(object):
TYPE_DIVIDEND = TransactionRecord.TYPE_DIVIDEND
TYPE_INCOME = TransactionRecord.TYPE_INCOME
TYPE_GIFT_RECEIVED = TransactionRecord.TYPE_GIFT_RECEIVED
TYPE_AIRDROP = TransactionRecord.TYPE_AIRDROP
TYPE_WITHDRAWAL = TransactionRecord.TYPE_WITHDRAWAL
TYPE_SPEND = TransactionRecord.TYPE_SPEND
TYPE_GIFT_SENT = TransactionRecord.TYPE_GIFT_SENT
TYPE_GIFT_SPOUSE = TransactionRecord.TYPE_GIFT_SPOUSE
TYPE_CHARITY_SENT = TransactionRecord.TYPE_CHARITY_SENT
TYPE_LOST = TransactionRecord.TYPE_LOST
TYPE_TRADE = TransactionRecord.TYPE_TRADE

def __init__(self, t_type, timestamp,
Expand Down
4 changes: 3 additions & 1 deletion bittytax/conv/output_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ class OutputCsv(OutputBase):
TransactionOutRecord.TYPE_DIVIDEND: 'Income',
TransactionOutRecord.TYPE_INCOME: 'Income',
TransactionOutRecord.TYPE_GIFT_RECEIVED: 'Gift',
TransactionOutRecord.TYPE_AIRDROP: 'Airdrop',
TransactionOutRecord.TYPE_WITHDRAWAL: 'Withdrawal',
TransactionOutRecord.TYPE_SPEND: 'Purchase',
TransactionOutRecord.TYPE_GIFT_SENT: 'Gift',
TransactionOutRecord.TYPE_GIFT_SPOUSE: 'Gift',
TransactionOutRecord.TYPE_GIFT_SPOUSE: 'Spouse',
TransactionOutRecord.TYPE_CHARITY_SENT: 'Donation',
TransactionOutRecord.TYPE_LOST: 'Lost',
TransactionOutRecord.TYPE_TRADE: 'Trade'}

def __init__(self, data_files, args):
Expand Down
6 changes: 4 additions & 2 deletions bittytax/conv/output_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,15 @@ class Worksheet(object):
TransactionOutRecord.TYPE_INTEREST,
TransactionOutRecord.TYPE_DIVIDEND,
TransactionOutRecord.TYPE_INCOME,
TransactionOutRecord.TYPE_GIFT_RECEIVED)
TransactionOutRecord.TYPE_GIFT_RECEIVED,
TransactionOutRecord.TYPE_AIRDROP)

SELL_LIST = (TransactionOutRecord.TYPE_WITHDRAWAL,
TransactionOutRecord.TYPE_SPEND,
TransactionOutRecord.TYPE_GIFT_SENT,
TransactionOutRecord.TYPE_GIFT_SPOUSE,
TransactionOutRecord.TYPE_CHARITY_SENT)
TransactionOutRecord.TYPE_CHARITY_SENT,
TransactionOutRecord.TYPE_LOST)
SHEETNAME_MAX_LEN = 31
MAX_COL_WIDTH = 30

Expand Down
4 changes: 2 additions & 2 deletions bittytax/conv/parsers/accointing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ..exceptions import UnexpectedTypeError

ACCOINTING_D_MAPPING = {'add_funds': TransactionOutRecord.TYPE_DEPOSIT,
'airdrop': TransactionOutRecord.TYPE_GIFT_RECEIVED,
'airdrop': TransactionOutRecord.TYPE_AIRDROP,
'bounty': TransactionOutRecord.TYPE_INCOME,
'gambling_income': TransactionOutRecord.TYPE_GIFT_RECEIVED,
'gift_received': TransactionOutRecord.TYPE_GIFT_RECEIVED,
Expand All @@ -30,7 +30,7 @@
'interest_paid': TransactionOutRecord.TYPE_SPEND,
'internal': TransactionOutRecord.TYPE_WITHDRAWAL,
'lending': None,
'lost': None,
'lost': TransactionOutRecord.TYPE_LOST,
'margin_fee': None,
'margin_loss': None,
'payment': TransactionOutRecord.TYPE_SPEND}
Expand Down
8 changes: 7 additions & 1 deletion bittytax/conv/parsers/binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,18 @@ def parse_binance_statements(data_rows, parser, **_kwargs):
row_dict = data_row.row_dict
data_row.timestamp = DataParser.parse_timestamp(row_dict['UTC_Time'])

if row_dict['Operation'] in ("Distribution", "Commission History", "Referrer rebates"):
if row_dict['Operation'] in ("Commission History", "Referrer rebates"):
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_GIFT_RECEIVED,
data_row.timestamp,
buy_quantity=row_dict['Change'],
buy_asset=row_dict['Coin'],
wallet=WALLET)
elif row_dict['Operation'] == "Distribution":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_AIRDROP,
data_row.timestamp,
buy_quantity=row_dict['Change'],
buy_asset=row_dict['Coin'],
wallet=WALLET)
elif row_dict['Operation'] == "Super BNB Mining":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_MINING,
data_row.timestamp,
Expand Down
2 changes: 1 addition & 1 deletion bittytax/conv/parsers/circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def parse_circle(data_row, parser, **_kwargs):
else None,
wallet=WALLET)
elif row_dict['Transaction Type'] == "fork":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_GIFT_RECEIVED,
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_AIRDROP,
data_row.timestamp,
buy_quantity=row_dict['To Amount'].strip('£€$'). \
split(' ')[0],
Expand Down
67 changes: 31 additions & 36 deletions bittytax/conv/parsers/cointracking.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# -*- coding: utf-8 -*-
# (c) Nano Nano Ltd 2020

from ...config import config
from ..out_record import TransactionOutRecord
from ..dataparser import DataParser
from ..exceptions import UnexpectedTypeError

WALLET = "CoinTracking"

COINTRACKING_TYPE_MAPPING = {'Trade': TransactionOutRecord.TYPE_TRADE,
'Deposit': TransactionOutRecord.TYPE_DEPOSIT,
'Income': TransactionOutRecord.TYPE_INCOME,
'Mining': TransactionOutRecord.TYPE_MINING,
'Gift/Tip': TransactionOutRecord.TYPE_GIFT_RECEIVED,
'Withdrawal': TransactionOutRecord.TYPE_WITHDRAWAL,
'Spend': TransactionOutRecord.TYPE_SPEND,
'Donation': TransactionOutRecord.TYPE_CHARITY_SENT,
'Gift': TransactionOutRecord.TYPE_GIFT_SENT,
'Stolen': TransactionOutRecord.TYPE_TRADE,
'Lost': TransactionOutRecord.TYPE_TRADE}
COINTRACKING_D_MAPPING = {'Income': TransactionOutRecord.TYPE_INCOME,
'Gift/Tip': TransactionOutRecord.TYPE_GIFT_RECEIVED,
'Reward/Bonus': TransactionOutRecord.TYPE_GIFT_RECEIVED,
'Mining': TransactionOutRecord.TYPE_MINING,
'Airdrop': TransactionOutRecord.TYPE_AIRDROP,
'Staking': TransactionOutRecord.TYPE_STAKING,
'Masternode': TransactionOutRecord.TYPE_STAKING}

COINTRACKING_W_MAPPING = {'Spend': TransactionOutRecord.TYPE_SPEND,
'Donation': TransactionOutRecord.TYPE_CHARITY_SENT,
'Gift': TransactionOutRecord.TYPE_GIFT_SENT,
'Stolen': TransactionOutRecord.TYPE_LOST,
'Lost': TransactionOutRecord.TYPE_LOST}

def parse_cointracking(data_row, parser, **_kwargs):
row_dict = data_row.row_dict
Expand All @@ -34,41 +35,38 @@ def parse_cointracking(data_row, parser, **_kwargs):
sell_asset=data_row.row[6],
sell_value=data_row.row[8],
wallet=wallet_name(row_dict['Exchange']))
elif row_dict['Type'] in ("Gift/Tip", "Income", "Mining"):
data_row.t_record = TransactionOutRecord(map_type(row_dict['Type']),
elif row_dict['Type'] == "Deposit":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_DEPOSIT,
data_row.timestamp,
buy_quantity=row_dict['Buy'],
buy_asset=data_row.row[2],
buy_value=data_row.row[4],
wallet=wallet_name(row_dict['Exchange']))
elif row_dict['Type'] in ("Lost", "Stolen"):
# No direct mapping, map as a trade for 0 GBP
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_TRADE,
data_row.timestamp,
buy_quantity=0,
buy_asset=config.ccy,
sell_quantity=row_dict['Sell'],
sell_asset=data_row.row[6],
wallet=wallet_name(row_dict['Exchange']))
elif row_dict['Type'] in ("Spend", "Gift", "Donation"):
data_row.t_record = TransactionOutRecord(map_type(row_dict['Type']),
data_row.timestamp,
sell_quantity=row_dict['Sell'],
sell_asset=data_row.row[6],
sell_value=data_row.row[8],
wallet=wallet_name(row_dict['Exchange']))
elif row_dict['Type'] == "Deposit":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_DEPOSIT,
elif row_dict['Type'] in COINTRACKING_D_MAPPING:
data_row.t_record = TransactionOutRecord(COINTRACKING_D_MAPPING[row_dict['Type']],
data_row.timestamp,
buy_quantity=row_dict['Buy'],
buy_asset=data_row.row[2],
buy_value=data_row.row[4],
wallet=wallet_name(row_dict['Exchange']))
elif row_dict['Type'] == "Withdrawal":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=row_dict['Sell'],
sell_asset=data_row.row[6],
wallet=wallet_name(row_dict['Exchange']))

elif row_dict['Type'] in COINTRACKING_W_MAPPING:
if row_dict['Type'] in ('Stolen', 'Lost'):
sell_value = None
else:
sell_value = data_row.row[8]

data_row.t_record = TransactionOutRecord(COINTRACKING_W_MAPPING[row_dict['Type']],
data_row.timestamp,
sell_quantity=row_dict['Sell'],
sell_asset=data_row.row[6],
sell_value=sell_value,
wallet=wallet_name(row_dict['Exchange']))
else:
raise UnexpectedTypeError(parser.in_header.index('Type'), 'Type', row_dict['Type'])

Expand All @@ -77,9 +75,6 @@ def wallet_name(wallet):
return WALLET
return wallet

def map_type(t_type):
return COINTRACKING_TYPE_MAPPING[t_type]

DataParser(DataParser.TYPE_ACCOUNTING,
"CoinTracking",
['Type', 'Buy', 'Cur.', 'Value in BTC', 'Value in GBP', 'Sell', 'Cur.', 'Value in BTC',
Expand Down
2 changes: 1 addition & 1 deletion bittytax/conv/parsers/poloniex.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def parse_poloniex_distributions(data_row, _parser, **_kwargs):
row_dict = data_row.row_dict
data_row.timestamp = DataParser.parse_timestamp(row_dict['date'])

data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_GIFT_RECEIVED,
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_AIRDROP,
data_row.timestamp,
buy_quantity=row_dict['amount'],
buy_asset=row_dict['currency'],
Expand Down
40 changes: 26 additions & 14 deletions bittytax/import_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,22 @@ class TransactionRow(object):
OPT = 'Optional'
MAN = 'Mandatory'

TYPE_VALIDATION = {TR.TYPE_DEPOSIT: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_MINING: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_STAKING: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_INTEREST: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_DIVIDEND: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_INCOME: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_GIFT_RECEIVED: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_WITHDRAWAL: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_SPEND: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_GIFT_SENT: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_GIFT_SPOUSE: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_CHARITY_SENT: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_TRADE: [MAN, MAN, MAN, OPT, MAN, MAN, OPT, OPT, OPT, OPT]}
TYPE_VALIDATION = {
TR.TYPE_DEPOSIT: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_MINING: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_STAKING: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_INTEREST: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_DIVIDEND: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_INCOME: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_GIFT_RECEIVED: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_AIRDROP: [MAN, MAN, MAN, OPT, None, None, None, OPT, OPT, OPT],
TR.TYPE_WITHDRAWAL: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_SPEND: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_GIFT_SENT: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_GIFT_SPOUSE: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_CHARITY_SENT: [MAN, None, None, None, MAN, MAN, OPT, OPT, OPT, OPT],
TR.TYPE_LOST: [MAN, None, None, None, MAN, MAN, OPT, None, None, None],
TR.TYPE_TRADE: [MAN, MAN, MAN, OPT, MAN, MAN, OPT, OPT, OPT, OPT]}

TRANSFER_TYPES = (TR.TYPE_DEPOSIT, TR.TYPE_WITHDRAWAL)

Expand Down Expand Up @@ -226,7 +229,16 @@ def parse(self):
if buy_asset:
buy = Buy(t_type, buy_quantity, buy_asset, buy_value)
if sell_asset:
sell = Sell(t_type, sell_quantity, sell_asset, sell_value)
if t_type == TR.TYPE_LOST:
if sell_value is None:
sell_value = Decimal(0)

sell = Sell(t_type, sell_quantity, sell_asset, sell_value)
if config.lost_buyback:
buy = Buy(t_type, sell_quantity, sell_asset, sell_value)
buy.acquisition = True
else:
sell = Sell(t_type, sell_quantity, sell_asset, sell_value)
if fee_asset:
# Fees are added as a separate spend transaction
fee = Sell(TR.TYPE_SPEND, fee_quantity, fee_asset, fee_value)
Expand Down
4 changes: 4 additions & 0 deletions bittytax/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ class TransactionRecord(object):
TYPE_DIVIDEND = 'Dividend'
TYPE_INCOME = 'Income'
TYPE_GIFT_RECEIVED = 'Gift-Received'
TYPE_AIRDROP = 'Airdrop'
TYPE_WITHDRAWAL = 'Withdrawal'
TYPE_SPEND = 'Spend'
TYPE_GIFT_SENT = 'Gift-Sent'
TYPE_GIFT_SPOUSE = 'Gift-Spouse'
TYPE_CHARITY_SENT = 'Charity-Sent'
TYPE_LOST = 'Lost'
TYPE_TRADE = 'Trade'

ALL_TYPES = (TYPE_DEPOSIT,
Expand All @@ -27,11 +29,13 @@ class TransactionRecord(object):
TYPE_INTEREST,
TYPE_DIVIDEND,
TYPE_GIFT_RECEIVED,
TYPE_AIRDROP,
TYPE_WITHDRAWAL,
TYPE_SPEND,
TYPE_GIFT_SENT,
TYPE_GIFT_SPOUSE,
TYPE_CHARITY_SENT,
TYPE_LOST,
TYPE_TRADE)

cnt = 0
Expand Down
7 changes: 5 additions & 2 deletions bittytax/tax.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class TaxCalculator(object):

NO_GAIN_NO_LOSS_TYPES = (Sell.TYPE_GIFT_SPOUSE, Sell.TYPE_CHARITY_SENT)

# These transactions are except from the "same day" & "bnb" rule
NO_MATCH_TYPES = (Sell.TYPE_GIFT_SPOUSE, Sell.TYPE_CHARITY_SENT, Sell.TYPE_LOST)

def __init__(self, transactions, tax_rules):
self.transactions = transactions
self.tax_rules = tax_rules
Expand All @@ -54,12 +57,12 @@ def pool_same_day(self):
unit='t',
desc="%spool same day%s" % (Fore.CYAN, Fore.GREEN),
disable=bool(config.debug or not sys.stdout.isatty())):
if isinstance(t, Buy) and t.acquisition:
if isinstance(t, Buy) and t.acquisition and t.t_type not in self.NO_MATCH_TYPES:
if (t.asset, t.timestamp.date()) not in buy_transactions:
buy_transactions[(t.asset, t.timestamp.date())] = t
else:
buy_transactions[(t.asset, t.timestamp.date())] += t
elif isinstance(t, Sell) and t.disposal and t.t_type not in self.NO_GAIN_NO_LOSS_TYPES:
elif isinstance(t, Sell) and t.disposal and t.t_type not in self.NO_MATCH_TYPES:
if (t.asset, t.timestamp.date()) not in sell_transactions:
sell_transactions[(t.asset, t.timestamp.date())] = t
else:
Expand Down
Loading

0 comments on commit d33d5d9

Please sign in to comment.