Skip to content

Commit 03ea68a

Browse files
committed
Add importer for Kraken
1 parent 0841aff commit 03ea68a

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-0
lines changed

my_import.py.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ from uabean.importers import (
2222
pumb_xls,
2323
procredit_business,
2424
procredit_xls,
25+
kraken,
2526
)
2627

2728
from uabean.hooks import detect_transfers
@@ -120,6 +121,7 @@ CONFIG = [
120121
("USD", "UA000000000000000000000000000"): "Assets:Procreditbank:Cash:USD",
121122
}
122123
),
124+
kraken.Importer(),
123125
]
124126

125127

src/uabean/importers/kraken.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Imports transactions from Kraken crypto exchange ledger csv file.
2+
3+
The CSV header is the following:
4+
"txid","refid","time","type","subtype","aclass","asset","amount","fee","balance"
5+
"""
6+
7+
8+
import csv
9+
10+
import beangulp
11+
import dateutil.parser
12+
from beancount.core import data, flags
13+
from beancount.core.amount import Amount
14+
from beancount.core.number import D
15+
16+
from uabean.importers.mixins import IdentifyMixin
17+
18+
19+
class Importer(IdentifyMixin, beangulp.Importer):
20+
FLAG = flags.FLAG_OKAY
21+
matchers = [
22+
("content", __doc__.split("\n")[-2]),
23+
("mime", "text/csv"),
24+
]
25+
26+
def __init__(
27+
self,
28+
spot_account="Assets:Kraken:Spot",
29+
staking_account="Assets:Kraken:Staking",
30+
fee_account="Expenses:Fees:Kraken",
31+
staking_income_account="Income:Staking:Kraken",
32+
):
33+
self.spot_account = spot_account
34+
self.staking_account = staking_account
35+
self.fee_account = fee_account
36+
self.staking_income_account = staking_income_account
37+
super().__init__()
38+
39+
def account(self, _):
40+
return "kraken"
41+
42+
def parse_date(self, s):
43+
return dateutil.parser.parse(s)
44+
45+
def extract(self, filename, existing_entries=None):
46+
entries = []
47+
balances = {}
48+
for index, row in enumerate(csv.DictReader(open(filename)), 1):
49+
meta = data.new_metadata(filename, index)
50+
entry = self.get_entry_from_row(row, meta, balances)
51+
if entry is not None:
52+
entries.append(entry)
53+
for (account, currency), (date, balance) in balances.items():
54+
entries.append(
55+
data.Balance(
56+
data.new_metadata(filename, -1),
57+
date.date() + dateutil.relativedelta.relativedelta(days=1),
58+
account,
59+
Amount(balance, currency),
60+
None,
61+
None,
62+
)
63+
)
64+
return entries
65+
66+
def get_entry_from_row(self, row, meta, balances):
67+
if not row["txid"]:
68+
return None
69+
date = self.parse_date(row["time"])
70+
meta["time"] = date.strftime("%H:%M:%S")
71+
postings = []
72+
match (row["type"], row["subtype"]):
73+
case ("deposit", "") | ("transfer", "spottostaking"):
74+
postings.append(
75+
data.Posting(
76+
self.spot_account,
77+
self.ammount_from_row(row),
78+
None,
79+
None,
80+
None,
81+
None,
82+
)
83+
)
84+
self.update_balance(
85+
balances,
86+
date,
87+
self.spot_account,
88+
row["asset"],
89+
D(row["balance"]),
90+
)
91+
case ("transfer", "stakingfromspot"):
92+
postings.append(
93+
data.Posting(
94+
self.staking_account,
95+
self.ammount_from_row(row),
96+
None,
97+
None,
98+
None,
99+
None,
100+
)
101+
)
102+
self.update_balance(
103+
balances,
104+
date,
105+
self.staking_account,
106+
row["asset"],
107+
D(row["balance"]),
108+
)
109+
case ("staking", ""):
110+
postings.append(
111+
data.Posting(
112+
self.staking_account,
113+
self.ammount_from_row(row),
114+
None,
115+
None,
116+
None,
117+
None,
118+
)
119+
)
120+
postings.append(
121+
data.Posting(
122+
self.staking_income_account,
123+
None,
124+
None,
125+
None,
126+
None,
127+
None,
128+
)
129+
)
130+
self.update_balance(
131+
balances,
132+
date,
133+
self.staking_account,
134+
row["asset"],
135+
D(row["balance"]),
136+
)
137+
case _:
138+
raise NotImplementedError(
139+
f"Unknown transaction type {row['type']} row['subtype']"
140+
)
141+
if float(row["fee"]):
142+
postings.append(
143+
data.Posting(
144+
self.fee_account,
145+
self.ammount_from_row(row),
146+
None,
147+
None,
148+
None,
149+
None,
150+
)
151+
)
152+
return data.Transaction(
153+
meta,
154+
date.date(),
155+
self.FLAG,
156+
"",
157+
"",
158+
data.EMPTY_SET,
159+
data.EMPTY_SET,
160+
postings,
161+
)
162+
163+
def ammount_from_row(self, row):
164+
return Amount(D(row["amount"]), row["asset"])
165+
166+
def update_balance(self, balances, date, account, currency, balance):
167+
if (account, currency) not in balances:
168+
balances[(account, currency)] = (date, balance)
169+
else:
170+
balances[(account, currency)] = max(
171+
balances[(account, currency)], (date, balance)
172+
)
173+
174+
175+
def get_test_importer():
176+
return Importer()
177+
178+
179+
if __name__ == "__main__":
180+
from beangulp.testing import main
181+
182+
main(get_test_importer())

tests/importers/kraken/ledgers.csv

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"txid","refid","time","type","subtype","aclass","asset","amount","fee","balance"
2+
"","TX-ID1","2000-01-01 08:48:50","deposit","","currency","USDC",1000.00000000,0.00000000,""
3+
"TX-ID2","TX-ID1","2000-01-01 08:57:47","deposit","","currency","USDC",1000.00000000,0.00000000,1000.00000000
4+
"","TX-id3","2000-01-01 09:00:21","withdrawal","","currency","USDC",-1000.00000000,0.00000000,""
5+
"TX-id4","TX-ID3","2000-01-01 09:00:22","transfer","spottostaking","currency","USDC",-1000.00000000,0.00000000,0.00000000
6+
"","TX-ID5","2000-01-01 09:00:30","deposit","","currency","USDC.M",1000.00000000,0.00000000,""
7+
"TX-ID6","TX-ID5","2000-01-01 09:00:32","transfer","stakingfromspot","currency","USDC.M",1000.00000000,0.00000000,1000.00000000
8+
"","TX-ID7","2001-01-02 14:30:21","deposit","","currency","USDC.M",1.12345678,0.00000000,""
9+
"TX-ID8","TX-ID7","2000-01-02 14:33:28","staking","","currency","USDC.M",1.12345678,0.00000000,1001.12345678
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
;; Account: kraken
2+
;; Date:
3+
;; Name:
4+
5+
2000-01-01 *
6+
time: "08:57:47"
7+
Assets:Kraken:Spot 1000.00000000 USDC
8+
9+
2000-01-01 *
10+
time: "09:00:22"
11+
Assets:Kraken:Spot -1000.00000000 USDC
12+
13+
2000-01-01 *
14+
time: "09:00:32"
15+
Assets:Kraken:Staking 1000.00000000 USDC.M
16+
17+
2000-01-02 balance Assets:Kraken:Spot 0.00000000 USDC
18+
19+
2000-01-02 *
20+
time: "14:33:28"
21+
Assets:Kraken:Staking 1.12345678 USDC.M
22+
Income:Staking:Kraken
23+
24+
2000-01-03 balance Assets:Kraken:Staking 1001.12345678 USDC.M

tests/importers/test_kraken.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from common import run_importer_test
2+
3+
from uabean.importers.kraken import get_test_importer
4+
5+
6+
def test_kraken_importer(capsys):
7+
run_importer_test(get_test_importer(), capsys)

0 commit comments

Comments
 (0)