Implement fetching account balances

This commit is contained in:
Mark Veidemanis 2022-04-09 19:59:22 +01:00
parent dc1b11bf1e
commit d93eb8e936
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 163 additions and 16 deletions

View File

@ -82,3 +82,18 @@ class Account(Model):
class AccountDetails(Model):
account: fields.Nested(Account)
class AccountBalanceAmount(Model):
amount: fields.Str()
currency: fields.Str()
class AccountBalances(Model):
balanceAmount: fields.Nested(AccountBalanceAmount)
balanceType: fields.Str()
referenceDate: fields.Date()
class AccountBalancesRoot(Model):
balances = fields.List(AccountBalances)

View File

@ -0,0 +1,14 @@
from serde import Model, fields
class AccountBalances(Model):
currency: fields.Str()
available: fields.Float()
current: fields.Float()
overdraft: fields.Float()
update_timestamp: fields.DateTime()
class AccountBalancesRoot(Model):
results: fields.List(AccountBalances)
status: fields.Str()

View File

@ -98,6 +98,19 @@ class Money(util.Base):
rates = self.get_rates_all()
return float(amount) / rates[currency]
def multiple_to_usd(self, currency_map):
"""
Convert multiple curencies to USD while saving API calls.
"""
rates = self.get_rates_all()
cumul = 0
for currency, amount in currency_map.items():
if currency == "USD":
cumul += float(amount)
else:
cumul += float(amount) / rates[currency]
return cumul
# TODO: move to money
def get_profit(self, trades=False):
"""

View File

@ -87,3 +87,16 @@ class Sinks(util.Base):
# {"EUR": {"IBAN": "xxx", "BIC": "xxx"},
# "GBP": {"SORT": "04-04-04", "ACCOUNT": "1922-2993"}}
# self.markets.distribute_account_details(currencies, account_infos)
def get_total_usd(self):
"""
Get the total balance of our accounts in USD.
"""
total_nordigen = self.nordigen.get_total_map()
total_truelayer = self.truelayer.get_total_map()
# Yes, we can save an API call by merging but I think this is clearer
total_nordigen_usd = self.money.multiple_to_usd(total_nordigen)
total_truelayer_usd = self.money.multiple_to_usd(total_truelayer)
return total_truelayer_usd + total_nordigen_usd

View File

@ -5,7 +5,7 @@ from twisted.internet.task import LoopingCall
import requests
from simplejson.errors import JSONDecodeError
from json import dumps, loads
from lib.serde.nordigen import TXRoot, AccessToken, Institutions, Agreement, Requisitions, AccountDetails
from lib.serde.nordigen import TXRoot, AccessToken, Institutions, Agreement, Requisitions, AccountDetails, AccountBalancesRoot
from serde import ValidationError
# Project imports
@ -242,9 +242,6 @@ class Nordigen(util.Base):
continue
accounts = self.get_accounts(req["id"])
for account_id in accounts:
# if not account_id in self.banks:
# print("account_id", account_id, "not in self.banks!")
# continue
account_info = self.get_account(account_id)
if not account_info:
continue
@ -281,7 +278,55 @@ class Nordigen(util.Base):
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/transactions/"
r = requests.get(path, headers=headers)
obj = TXRoot.from_json(r.content)
try:
obj = TXRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["transactions"]["booked"]
self.normalise_transactions(parsed)
return parsed
def get_balance(self, account_id):
"""
Get the balance and currency of an account.
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
headers = {"accept": "application/json", "Authorization": f"Bearer {self.token}"}
path = f"{settings.Nordigen.Base}/accounts/{account_id}/balances/"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["balances"]
total = 0
currency = None
for entry in parsed:
if currency:
if not currency == entry["balanceAmount"]["currency"]:
self.log.error("Different currencies in balance query.")
return
total += float(entry["balanceAmount"]["amount"])
currency = entry["balanceAmount"]["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for account_id in self.banks:
currency, amount = self.get_balance(account_id)
if not amount:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

View File

@ -6,7 +6,9 @@ import requests
from simplejson.errors import JSONDecodeError
from time import time
from json import dumps, loads
from lib.serde.truelayer import AccountBalancesRoot
import urllib
from serde import ValidationError
# Project imports
from settings import settings
@ -274,8 +276,55 @@ class TrueLayer(util.Base):
parsed = r.json()
except JSONDecodeError:
self.log.error(f"Error parsing transactions response: {r.content}")
return False
return (False, False)
if "results" in parsed:
return parsed["results"]
else:
return False
return (False, False)
def get_balance(self, bank, account_id):
"""
Get the balance of an account.
:param bank: the bank to check
:param account_id: the account ID
:return: tuple of (currency, amount)
:rtype: tuple
"""
token = self.get_key(bank)
headers = {"Authorization": f"Bearer {token}"}
path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/balance"
r = requests.get(path, headers=headers)
try:
obj = AccountBalancesRoot.from_json(r.content)
except ValidationError as err:
self.log.error(f"Validation error: {err}")
return
parsed = obj.to_dict()["results"]
total = 0
currency = None
for entry in parsed:
if currency:
if not currency == entry["currency"]:
self.log.error("Different currencies in balance query.")
return
total += entry["available"]
currency = entry["currency"]
return (currency, total)
def get_total_map(self):
"""
Return a dictionary keyed by currencies with the amounts as values.
:return: dict keyed by currency, values are amounts
:rtype: dict
"""
totals = {}
for bank in self.banks:
for account_id in self.banks[bank]:
currency, amount = self.get_balance(bank, account_id)
if not amount:
continue
if currency in totals:
totals[currency] += amount
else:
totals[currency] = amount
return totals

View File

@ -263,8 +263,11 @@ class Transactions(util.Base):
matching_refs = []
# TODO: use get_ref_map in this function instead of calling get_ref multiple times
for ref in refs:
print(f"ITER REF {ref}")
stored_trade = self.get_ref(ref)
print(f"ITER REF STORED TRADE {stored_trade}")
if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount):
print(f"APPENDING STORED TRADE AS MATCH {stored_trade}")
matching_refs.append(stored_trade)
if len(matching_refs) != 1:
self.log.error(f"Find trade returned multiple results for TXID {txid}: {matching_refs}")
@ -368,7 +371,7 @@ class Transactions(util.Base):
:return: value in USD
:rtype float:
"""
# TODO: get Sink totals
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]:
return False
@ -391,9 +394,7 @@ class Transactions(util.Base):
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
# total_usd = total_usd_agora + total_usd_revolut
# TODO: add sinks value here
total_usd = total_usd_agora
total_usd = total_usd_agora + total_sinks_usd
cast_es = {
"price_usd": total_usd,
"total_usd_agora_xmr": total_usd_agora_xmr,
@ -416,8 +417,7 @@ class Transactions(util.Base):
:return: ((total SEK, total USD, total GBP), (total XMR USD, total BTC USD), (total XMR, total BTC))
:rtype: tuple(tuple(float, float, float), tuple(float, float), tuple(float, float))
"""
# TODO: get sinks value here
# total_usd_revolut = self.revolut.get_total_usd()
total_sinks_usd = self.sinks.get_total_usd()
agora_wallet_xmr = self.agora.agora.wallet_balance_xmr()
if not agora_wallet_xmr["success"]:
self.log.error("Could not get Agora XMR wallet total.")
@ -442,9 +442,7 @@ class Transactions(util.Base):
# Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
# total_usd = total_usd_agora + total_usd_revolut
# TODO: add sinks value here
total_usd = total_usd_agora
total_usd = total_usd_agora + total_sinks_usd
# Convert the total USD price to GBP and SEK
rates = self.money.get_rates_all()