Skip to content

Commit 6c77252

Browse files
Merge branch 'symbol:main' into block_lib
2 parents deedc1b + 434ec23 commit 6c77252

2 files changed

Lines changed: 152 additions & 3 deletions

File tree

history/merger.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def load(self, filename):
4343
snapshot = client.pod.AugmentedTransactionSnapshot()
4444
snapshot.__dict__.update(row)
4545
raw_timestamp = snapshot.timestamp
46-
4746
snapshot.fix_types()
4847

4948
price_snapshot = self.price_map[snapshot.timestamp.date()]
@@ -123,12 +122,12 @@ def main():
123122
parser.add_argument('--ticker', help='ticker symbol', default='nem')
124123
parser.add_argument('--currency', help='fiat currency', default='usd')
125124
parser.add_argument('--human-readable', help='outputs a more human readable format', action='store_true')
126-
127125
args = parser.parse_args()
126+
128127
transactions_loader = TransactionsLoader(args.input, args.ticker, args.currency, args.human_readable)
129128
transactions_loader.load_price_map()
130129

131-
for filepath in Path(args.input).iterdir():
130+
for filepath in Path(args.input).glob('**/*.csv'):
132131
if not filepath.name.startswith(args.ticker):
133132
transactions_loader.load(filepath.name)
134133

history/merger_taxbit.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import argparse
2+
import csv
3+
import datetime
4+
from pathlib import Path
5+
6+
from zenlog import log
7+
8+
import client.pod
9+
10+
11+
class TransactionsLoader():
12+
def __init__(self, ticker, start_date, end_date):
13+
self.ticker = ticker
14+
self.start_date = start_date
15+
self.end_date = end_date
16+
17+
self.transaction_snapshots = []
18+
19+
def load(self, filepath):
20+
log.info(f'loading transactions from {filepath}')
21+
22+
with open(filepath, 'rt', encoding='utf8') as infile:
23+
csv_reader = csv.DictReader(infile)
24+
25+
for row in csv_reader:
26+
self._process_row(row)
27+
28+
def _process_row(self, row):
29+
snapshot = client.pod.AugmentedTransactionSnapshot()
30+
snapshot.__dict__.update(row)
31+
raw_timestamp = snapshot.timestamp
32+
snapshot.fix_types()
33+
34+
if self.start_date and snapshot.timestamp.date() < self.start_date:
35+
return
36+
37+
if snapshot.timestamp.date() > self.end_date:
38+
return
39+
40+
if 0 == snapshot.amount and 0 == snapshot.fee_paid:
41+
return
42+
43+
self._fixup_tag(snapshot)
44+
snapshot.timestamp = datetime.datetime.fromisoformat(raw_timestamp).isoformat()
45+
snapshot.timestamp = snapshot.timestamp.replace('+00:00', 'Z')
46+
self.transaction_snapshots.append(snapshot)
47+
48+
@staticmethod
49+
def _fixup_tag(snapshot):
50+
snapshot.amount_sent = 0
51+
snapshot.amount_received = 0
52+
53+
if snapshot.amount < 0:
54+
snapshot.tag = 'Expense'
55+
snapshot.amount_sent = -snapshot.amount
56+
elif snapshot.amount > 0:
57+
snapshot.tag = 'Income'
58+
snapshot.amount_received = snapshot.amount
59+
elif 0 != snapshot.fee_paid:
60+
# TaxBit does not support 'fee only' transactions, so treat as expense
61+
snapshot.tag = 'Expense'
62+
snapshot.amount_sent = -snapshot.fee_paid
63+
snapshot.fee_paid = 0
64+
65+
if snapshot.fee_paid:
66+
snapshot.fee_paid = -snapshot.fee_paid
67+
68+
def save(self, filename):
69+
log.info(f'saving merged report to {filename}')
70+
71+
self.transaction_snapshots.sort(key=lambda snapshot: snapshot.timestamp)
72+
73+
# count the number of times each transaction hash appears
74+
# if it occurs multiple times, assume it is a transfer of funds between (owned) accounts
75+
transaction_hash_counts = {}
76+
for snapshot in self.transaction_snapshots:
77+
transaction_hash_counts[snapshot.hash] = transaction_hash_counts.get(snapshot.hash, 0) + 1
78+
79+
with open(filename, 'wt', newline='', encoding='utf8') as outfile:
80+
column_headers = [
81+
'Date and Time',
82+
'Transaction Type',
83+
'Sent Quantity',
84+
'Sent Currency',
85+
'Sending Source',
86+
'Received Quantity',
87+
'Received Currency',
88+
'Receiving Destination',
89+
'Fee',
90+
'Fee Currency',
91+
'Exchange Transaction ID',
92+
'Blockchain Transaction Hash'
93+
]
94+
95+
csv_writer = csv.writer(outfile)
96+
csv_writer.writerow(column_headers)
97+
98+
# map of transaction hash to last id
99+
# this is only populated for duplicate hashes in order to generate a unique postfix for disambiguation
100+
transaction_hash_to_last_id = {}
101+
102+
taxbit_ticker = 'XEM' if 'nem' == self.ticker else 'XYM'
103+
for snapshot in self.transaction_snapshots:
104+
transaction_id = None # used for disambiguation of duplicate hashes
105+
if transaction_hash_counts[snapshot.hash] > 1:
106+
transaction_id = transaction_hash_to_last_id.get(snapshot.hash, 0) + 1
107+
transaction_hash_to_last_id[snapshot.hash] = transaction_id
108+
109+
is_income = 'Income' == snapshot.tag
110+
if transaction_id:
111+
snapshot.tag = 'Transfer In' if is_income else 'Transfer Out'
112+
113+
csv_writer.writerow([
114+
snapshot.timestamp,
115+
snapshot.tag,
116+
'' if is_income else snapshot.amount_sent,
117+
'' if is_income else taxbit_ticker,
118+
'' if is_income else f'{taxbit_ticker} Wallet',
119+
'' if not is_income else snapshot.amount_received,
120+
'' if not is_income else taxbit_ticker,
121+
'' if not is_income else f'{taxbit_ticker} Wallet',
122+
'' if not snapshot.fee_paid else snapshot.fee_paid,
123+
'' if not snapshot.fee_paid else taxbit_ticker,
124+
'',
125+
f'{snapshot.hash}-{transaction_id}' if transaction_id else str(snapshot.hash)
126+
])
127+
128+
129+
def main():
130+
parser = argparse.ArgumentParser(description='generates a merged report that can be imported into TaxBit')
131+
parser.add_argument('--input', help='input directory', required=True)
132+
parser.add_argument('--output', help='output filename', required=True)
133+
parser.add_argument('--ticker', help='ticker symbol', default='nem')
134+
parser.add_argument('--start-date', help='start date')
135+
parser.add_argument('--end-date', help='end date', default=datetime.datetime.today())
136+
args = parser.parse_args()
137+
138+
start_date = datetime.date.fromisoformat(args.start_date) if args.start_date else None
139+
end_date = datetime.date.fromisoformat(args.end_date)
140+
transactions_loader = TransactionsLoader(args.ticker, start_date, end_date)
141+
142+
for filepath in Path(args.input).glob('**/*.csv'):
143+
if not filepath.name.startswith(args.ticker):
144+
transactions_loader.load(filepath)
145+
146+
transactions_loader.save(args.output)
147+
148+
149+
if '__main__' == __name__:
150+
main()

0 commit comments

Comments
 (0)