from datetime import date
from os import path
from typing import Any
import beangulp
import bitstamp.client
import yaml
from beancount.core import amount, data
from beancount.core.number import MISSING, D
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from tariochbctools.importers.general.deduplication import ReferenceDuplicatesComparator
from tariochbctools.importers.general.priceLookup import PriceLookup
[docs]
class Importer(beangulp.Importer):
"""An importer for Bitstamp."""
[docs]
def identify(self, filepath: str) -> bool:
return path.basename(filepath).endswith("bitstamp.yaml")
[docs]
def account(self, filepath: str) -> data.Account:
return ""
[docs]
def fetchSingle(self, trx: dict[str, Any]) -> data.Transaction:
id = int(trx["id"])
type = int(trx["type"])
date = parse(trx["datetime"]).date()
posAmt = D("0")
posCcy: str | None = None
negAmt = D("0")
negCcy: str | None = None
for ccy in self.currencies:
if ccy in trx:
amt = D(trx[ccy])
if amt > 0:
posAmt = amt
posCcy = ccy.upper()
elif amt < 0:
negAmt = amt
negCcy = ccy.upper()
postings: list[data.Posting]
if type == 0:
narration = "Deposit"
posCcy = self.requiredCurrency(posCcy, id, "positive")
cost = data.CostSpec(
self.priceLookup.fetchPriceAmount(posCcy, date),
None,
"CHF",
None,
None,
False,
)
postings = [
data.Posting(
self._account + ":" + posCcy,
amount.Amount(posAmt, posCcy),
cost,
None,
None,
None,
),
]
elif type == 1:
narration = "Withdrawal"
negCcy = self.requiredCurrency(negCcy, id, "negative")
postings = [
data.Posting(
self._account + ":" + negCcy,
amount.Amount(negAmt, negCcy),
None,
None,
None,
None,
),
]
elif type == 2:
posCcy = self.requiredCurrency(posCcy, id, "positive")
negCcy = self.requiredCurrency(negCcy, id, "negative")
fee = D(trx["fee"])
if posCcy.lower() + "_" + negCcy.lower() in trx:
feeCcy = negCcy
negAmt -= fee
else:
feeCcy = posCcy
posAmt -= fee
rateFiatCcy = self.priceLookup.fetchPriceAmount(feeCcy, date)
if rateFiatCcy is None:
raise ValueError(
f"There is no price for {feeCcy} on {date} (transaction {id})"
)
posCcyCost: data.CostSpec | None
posCcyPrice: amount.Amount | None
negCcyCost: data.CostSpec | None
negCcyPrice: amount.Amount | None
if feeCcy == posCcy:
posCcyCost = None
posCcyPrice = amount.Amount(rateFiatCcy, "CHF")
# MISSING marks what beancount has to infer, its type hints do not allow it here
negCcyCost = data.CostSpec(MISSING, None, MISSING, None, None, False) # type: ignore[arg-type]
negCcyPrice = None
else:
posCcyCost = data.CostSpec(
None, D(-negAmt * rateFiatCcy), "CHF", None, None, False
)
posCcyPrice = None
negCcyCost = None
negCcyPrice = amount.Amount(rateFiatCcy, "CHF")
narration = "Trade"
postings = [
data.Posting(
self._account + ":" + posCcy,
amount.Amount(posAmt, posCcy),
posCcyCost,
posCcyPrice,
None,
None,
),
data.Posting(
self._account + ":" + negCcy,
amount.Amount(negAmt, negCcy),
negCcyCost,
negCcyPrice,
None,
None,
),
]
if float(fee) > 0:
postings.append(
data.Posting(
self.otherExpensesAccount,
amount.Amount(round(fee * rateFiatCcy, 2), "CHF"),
None,
None,
None,
None,
)
)
postings.append(
data.Posting(self.capGainAccount, None, None, None, None, None)
)
else:
raise ValueError("Transaction type " + str(type) + " is not handled")
meta = data.new_metadata("bitstamp", id, {"ref": str(id)})
return data.Transaction(
meta, date, "*", "", narration, data.EMPTY_SET, data.EMPTY_SET, postings
)
[docs]
def requiredCurrency(self, currency: str | None, id: int, sign: str) -> str:
if currency is None:
raise ValueError(
f"Transaction {id} has no {sign} amount in the currencies {self.currencies}"
)
return currency
cmp = ReferenceDuplicatesComparator()